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,416 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from abc import ABC
5
+ from functools import lru_cache
6
+ from sys import version_info
7
+ from typing import Any
8
+
9
+ import requests
10
+
11
+ from localstack_cli import config
12
+
13
+ from ..constants import LOCALSTACK_VENV_FOLDER, MAVEN_REPO_URL
14
+ from ..utils.archives import download_and_extract
15
+ from ..utils.files import chmod_r, chown_r, mkdir, rm_rf
16
+ from ..utils.http import download, get_proxies
17
+ from ..utils.run import is_root, run
18
+ from ..utils.venv import VirtualEnvironment
19
+ from .api import InstallTarget, PackageException, PackageInstaller
20
+
21
+ LOG = logging.getLogger(__name__)
22
+
23
+
24
+ class SystemNotSupportedException(PackageException):
25
+ """Exception indicating that the current system is not allowed."""
26
+
27
+ pass
28
+
29
+
30
+ class ExecutableInstaller(PackageInstaller, ABC):
31
+ """
32
+ This installer simply adds a clean interface for accessing a downloaded executable directly
33
+ """
34
+
35
+ def get_executable_path(self) -> str | None:
36
+ """
37
+ :return: the path to the downloaded binary or None if it's not yet downloaded / installed.
38
+ """
39
+ install_dir = self.get_installed_dir()
40
+ if install_dir:
41
+ return self._get_install_marker_path(install_dir)
42
+ return None
43
+
44
+
45
+ class DownloadInstaller(ExecutableInstaller):
46
+ def __init__(self, name: str, version: str):
47
+ super().__init__(name, version)
48
+
49
+ def _get_download_url(self) -> str:
50
+ raise NotImplementedError()
51
+
52
+ def _get_install_marker_path(self, install_dir: str) -> str:
53
+ url = self._get_download_url()
54
+ binary_name = os.path.basename(url)
55
+ return os.path.join(install_dir, binary_name)
56
+
57
+ def _install(self, target: InstallTarget) -> None:
58
+ target_directory = self._get_install_dir(target)
59
+ mkdir(target_directory)
60
+ download_url = self._get_download_url()
61
+ target_path = self._get_install_marker_path(target_directory)
62
+ download(download_url, target_path)
63
+
64
+
65
+ class ArchiveDownloadAndExtractInstaller(ExecutableInstaller):
66
+ def __init__(
67
+ self,
68
+ name: str,
69
+ version: str,
70
+ extract_single_directory: bool = False,
71
+ ):
72
+ """
73
+ :param name: technical package name, f.e. "opensearch"
74
+ :param version: version of the package to install
75
+ :param extract_single_directory: whether to extract files from single root folder in the archive
76
+ """
77
+ super().__init__(name, version)
78
+ self.extract_single_directory = extract_single_directory
79
+
80
+ def _get_install_marker_path(self, install_dir: str) -> str:
81
+ raise NotImplementedError()
82
+
83
+ def _get_download_url(self) -> str:
84
+ raise NotImplementedError()
85
+
86
+ def _get_checksum_url(self) -> str | None:
87
+ """
88
+ Checksum URL for the archive. This is used to verify the integrity of the downloaded archive.
89
+ This method can be implemented by subclasses to provide the correct URL for the checksum file.
90
+ If not implemented, checksum verification will be skipped.
91
+
92
+ :return: URL to the checksum file for the archive, or None if not available.
93
+ """
94
+ return None
95
+
96
+ def get_installed_dir(self) -> str | None:
97
+ installed_dir = super().get_installed_dir()
98
+ subdir = self._get_archive_subdir()
99
+
100
+ # If the specific installer defines a subdirectory, we return the subdirectory.
101
+ # f.e. /var/lib/localstack/lib/amazon-mq/5.16.5/apache-activemq-5.16.5/
102
+ if installed_dir and subdir:
103
+ return os.path.join(installed_dir, subdir)
104
+
105
+ return installed_dir
106
+
107
+ def _get_archive_subdir(self) -> str | None:
108
+ """
109
+ :return: name of the subdirectory contained in the archive or none if the package content is at the root level
110
+ of the archive
111
+ """
112
+ return None
113
+
114
+ def get_executable_path(self) -> str | None:
115
+ subdir = self._get_archive_subdir()
116
+ if subdir is None:
117
+ return super().get_executable_path()
118
+ else:
119
+ install_dir = self.get_installed_dir()
120
+ if install_dir:
121
+ install_dir = install_dir[: -len(subdir)]
122
+ return self._get_install_marker_path(install_dir)
123
+ return None
124
+
125
+ def _handle_single_directory_extraction(self, target_directory: str) -> None:
126
+ """
127
+ Handle extraction of archives that contain a single root directory.
128
+ Moves the contents up one level if extract_single_directory is True.
129
+
130
+ :param target_directory: The target extraction directory
131
+ :return: None
132
+ """
133
+ if not self.extract_single_directory:
134
+ return
135
+
136
+ dir_contents = os.listdir(target_directory)
137
+ if len(dir_contents) != 1:
138
+ return
139
+ target_subdir = os.path.join(target_directory, dir_contents[0])
140
+ if not os.path.isdir(target_subdir):
141
+ return
142
+ os.rename(target_subdir, f"{target_directory}.backup")
143
+ rm_rf(target_directory)
144
+ os.rename(f"{target_directory}.backup", target_directory)
145
+
146
+ def _download_archive(
147
+ self,
148
+ target: InstallTarget,
149
+ download_url: str,
150
+ ) -> None:
151
+ target_directory = self._get_install_dir(target)
152
+ mkdir(target_directory)
153
+ download_url = download_url or self._get_download_url()
154
+ archive_name = os.path.basename(download_url)
155
+ archive_path = os.path.join(config.dirs.tmp, archive_name)
156
+
157
+ # Get checksum info if available
158
+ checksum_url = self._get_checksum_url()
159
+
160
+ try:
161
+ download_and_extract(
162
+ download_url,
163
+ retries=3,
164
+ tmp_archive=archive_path,
165
+ target_dir=target_directory,
166
+ checksum_url=checksum_url,
167
+ )
168
+ self._handle_single_directory_extraction(target_directory)
169
+ finally:
170
+ rm_rf(archive_path)
171
+
172
+ def _install(self, target: InstallTarget) -> None:
173
+ self._download_archive(target, self._get_download_url())
174
+
175
+
176
+ class PermissionDownloadInstaller(DownloadInstaller, ABC):
177
+ def _install(self, target: InstallTarget) -> None:
178
+ super()._install(target)
179
+ chmod_r(self.get_executable_path(), 0o777) # type: ignore[arg-type]
180
+
181
+
182
+ class GitHubReleaseInstaller(PermissionDownloadInstaller):
183
+ """
184
+ Installer which downloads an asset from a GitHub project's tag.
185
+ """
186
+
187
+ def __init__(self, name: str, tag: str, github_slug: str):
188
+ super().__init__(name, tag)
189
+ self.github_tag_url = (
190
+ f"https://api.github.com/repos/{github_slug}/releases/tags/{self.version}"
191
+ )
192
+
193
+ @lru_cache
194
+ def _get_download_url(self) -> str:
195
+ asset_name = self._get_github_asset_name()
196
+ # try to use a token when calling the GH API for increased API rate limits
197
+ headers = None
198
+ gh_token = os.environ.get("GITHUB_API_TOKEN")
199
+ if gh_token:
200
+ headers = {"authorization": f"Bearer {gh_token}"}
201
+ response = requests.get(self.github_tag_url, headers=headers, proxies=get_proxies())
202
+ if not response.ok:
203
+ raise PackageException(
204
+ f"Could not get list of releases from {self.github_tag_url}: {response.text}"
205
+ )
206
+ github_release = response.json()
207
+ download_url = None
208
+ for asset in github_release.get("assets", []):
209
+ # find the correct binary in the release
210
+ if asset["name"] == asset_name:
211
+ download_url = asset["browser_download_url"]
212
+ break
213
+ if download_url is None:
214
+ raise PackageException(
215
+ f"Could not find required binary {asset_name} in release {self.github_tag_url}"
216
+ )
217
+ return download_url
218
+
219
+ def _get_install_marker_path(self, install_dir: str) -> str:
220
+ # Use the GitHub asset name instead of the download URL (since the download URL needs to be fetched online).
221
+ return os.path.join(install_dir, self._get_github_asset_name())
222
+
223
+ def _get_github_asset_name(self) -> str:
224
+ """
225
+ Determines the name of the asset to download.
226
+ The asset name must be determinable without having any online data (because it is used in offline scenarios to
227
+ determine if the package is already installed).
228
+
229
+ :return: name of the asset to download from the GitHub project's tag / version
230
+ """
231
+ raise NotImplementedError()
232
+
233
+
234
+ class NodePackageInstaller(ExecutableInstaller):
235
+ """Package installer for Node / NPM packages."""
236
+
237
+ def __init__(
238
+ self,
239
+ package_name: str,
240
+ version: str,
241
+ package_spec: str | None = None,
242
+ main_module: str = "main.js",
243
+ ):
244
+ """
245
+ Initializes the Node / NPM package installer.
246
+ :param package_name: npm package name
247
+ :param version: version of the package which should be installed
248
+ :param package_spec: optional package spec for the installation.
249
+ If not set, the package name and version will be used for the installation.
250
+ :param main_module: main module file of the package
251
+ """
252
+ super().__init__(package_name, version)
253
+ self.package_name = package_name
254
+ # If the package spec is not explicitly set (f.e. to a repo), we build it and pin the version
255
+ self.package_spec = package_spec or f"{self.package_name}@{version}"
256
+ self.main_module = main_module
257
+
258
+ def _get_install_marker_path(self, install_dir: str) -> str:
259
+ return os.path.join(install_dir, "node_modules", self.package_name, self.main_module)
260
+
261
+ def _install(self, target: InstallTarget) -> None:
262
+ target_dir = self._get_install_dir(target)
263
+
264
+ run(
265
+ [
266
+ "npm",
267
+ "install",
268
+ "--prefix",
269
+ target_dir,
270
+ self.package_spec,
271
+ ]
272
+ )
273
+ # npm 9+ does _not_ set the ownership of files anymore if run as root
274
+ # - https://github.blog/changelog/2022-10-24-npm-v9-0-0-released/
275
+ # - https://github.com/npm/cli/pull/5704
276
+ # - https://github.com/localstack/localstack/issues/7620
277
+ if is_root():
278
+ # if the package was installed as root, set the ownership manually
279
+ LOG.debug("Setting ownership root:root on %s", target_dir)
280
+ chown_r(target_dir, "root")
281
+
282
+
283
+ LOCALSTACK_VENV = VirtualEnvironment(LOCALSTACK_VENV_FOLDER)
284
+
285
+
286
+ class PythonPackageInstaller(PackageInstaller):
287
+ """
288
+ Package installer which allows the runtime-installation of additional python packages used by certain services.
289
+ f.e. vosk as offline speech recognition toolkit (which is ~7MB in size compressed and ~26MB uncompressed).
290
+ """
291
+
292
+ normalized_name: str
293
+ """Normalized package name according to PEP440."""
294
+
295
+ def __init__(self, name: str, version: str, *args: Any, **kwargs: Any):
296
+ super().__init__(name, version, *args, **kwargs)
297
+ self.normalized_name = self._normalize_package_name(name)
298
+
299
+ def _normalize_package_name(self, name: str) -> str:
300
+ """
301
+ Normalized the Python package name according to PEP440.
302
+ https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization
303
+ """
304
+ return re.sub(r"[-_.]+", "-", name).lower()
305
+
306
+ def _get_install_dir(self, target: InstallTarget) -> str:
307
+ # all python installers share a venv
308
+ return os.path.join(target.value, "python-packages")
309
+
310
+ def _get_install_marker_path(self, install_dir: str) -> str:
311
+ python_subdir = f"python{version_info[0]}.{version_info[1]}"
312
+ dist_info_dir = f"{self.normalized_name}-{self.version}.dist-info"
313
+ # the METADATA file is mandatory, use it as install marker
314
+ return os.path.join(
315
+ install_dir, "lib", python_subdir, "site-packages", dist_info_dir, "METADATA"
316
+ )
317
+
318
+ def _get_venv(self, target: InstallTarget) -> VirtualEnvironment:
319
+ venv_dir = self._get_install_dir(target)
320
+ return VirtualEnvironment(venv_dir)
321
+
322
+ def _prepare_installation(self, target: InstallTarget) -> None:
323
+ # make sure the venv is properly set up before installing the package
324
+ venv = self._get_venv(target)
325
+ if not venv.exists:
326
+ LOG.info("creating virtual environment at %s", venv.venv_dir)
327
+ venv.create()
328
+ LOG.info("adding localstack venv path %s", venv.venv_dir)
329
+ venv.add_pth("localstack-venv", LOCALSTACK_VENV)
330
+ LOG.debug("injecting venv into path %s", venv.venv_dir)
331
+ venv.inject_to_sys_path()
332
+
333
+ def _install(self, target: InstallTarget) -> None:
334
+ venv = self._get_venv(target)
335
+ python_bin = os.path.join(venv.venv_dir, "bin/python")
336
+
337
+ # run pip via the python binary of the venv
338
+ run([python_bin, "-m", "pip", "install", f"{self.name}=={self.version}"], print_error=False)
339
+
340
+ def _setup_existing_installation(self, target: InstallTarget) -> None:
341
+ """If the venv is already present, it just needs to be initialized once."""
342
+ self._prepare_installation(target)
343
+
344
+
345
+ class MavenDownloadInstaller(DownloadInstaller):
346
+ """The packageURL is easy copy/pastable from the Maven central repository and the first package URL
347
+ defines the package name and version.
348
+ Example package_url: pkg:maven/software.amazon.event.ruler/event-ruler@1.7.3
349
+ => name: event-ruler
350
+ => version: 1.7.3
351
+ """
352
+
353
+ # Example: software.amazon.event.ruler
354
+ group_id: str
355
+ # Example: event-ruler
356
+ artifact_id: str
357
+
358
+ # Custom installation directory
359
+ install_dir_suffix: str | None
360
+
361
+ def __init__(self, package_url: str, install_dir_suffix: str | None = None):
362
+ self.group_id, self.artifact_id, version = parse_maven_package_url(package_url)
363
+ super().__init__(self.artifact_id, version)
364
+ self.install_dir_suffix = install_dir_suffix
365
+
366
+ def _get_download_url(self) -> str:
367
+ group_id_path = self.group_id.replace(".", "/")
368
+ return f"{MAVEN_REPO_URL}/{group_id_path}/{self.artifact_id}/{self.version}/{self.artifact_id}-{self.version}.jar"
369
+
370
+ def _get_install_dir(self, target: InstallTarget) -> str:
371
+ """Allow to overwrite the default installation directory.
372
+ This enables downloading transitive dependencies into the same directory.
373
+ """
374
+ if self.install_dir_suffix:
375
+ return os.path.join(target.value, self.install_dir_suffix)
376
+ else:
377
+ return super()._get_install_dir(target)
378
+
379
+
380
+ class MavenPackageInstaller(MavenDownloadInstaller):
381
+ """Package installer for downloading Maven JARs, including optional dependencies.
382
+ The first Maven package is used as main LPM package and other dependencies are installed additionally.
383
+ Follows the Maven naming conventions: https://maven.apache.org/guides/mini/guide-naming-conventions.html
384
+ """
385
+
386
+ # Installers for Maven dependencies
387
+ dependencies: list[MavenDownloadInstaller]
388
+
389
+ def __init__(self, *package_urls: str):
390
+ super().__init__(package_urls[0])
391
+ self.dependencies = []
392
+
393
+ # Create installers for dependencies
394
+ for package_url in package_urls[1:]:
395
+ install_dir_suffix = os.path.join(self.name, self.version)
396
+ self.dependencies.append(MavenDownloadInstaller(package_url, install_dir_suffix))
397
+
398
+ def _install(self, target: InstallTarget) -> None:
399
+ # Install all dependencies first
400
+ for dependency in self.dependencies:
401
+ dependency._install(target)
402
+ # Install the main Maven package once all dependencies are installed.
403
+ # This main package indicates whether all dependencies are installed.
404
+ super()._install(target)
405
+
406
+
407
+ def parse_maven_package_url(package_url: str) -> tuple[str, str, str]:
408
+ """Example: parse_maven_package_url("pkg:maven/software.amazon.event.ruler/event-ruler@1.7.3")
409
+ -> software.amazon.event.ruler, event-ruler, 1.7.3
410
+ """
411
+ parts = package_url.split("/")
412
+ group_id = parts[1]
413
+ sub_parts = parts[2].split("@")
414
+ artifact_id = sub_parts[0]
415
+ version = sub_parts[1]
416
+ return group_id, artifact_id, version
File without changes
File without changes
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,213 @@
1
+ import base64
2
+ import getpass
3
+ import json
4
+ import logging
5
+ import sys
6
+ import time
7
+ from typing import Any
8
+
9
+ import jwt
10
+ from localstack_cli.constants import API_ENDPOINT
11
+ from localstack_cli.pro.core import config as pro_config
12
+ from localstack_cli.pro.core.bootstrap.licensingv2 import (
13
+ ApiKeyCredentials,
14
+ get_credentials_from_environment,
15
+ )
16
+ from localstack_cli.pro.core.bootstrap.licensingv2 import (
17
+ AuthToken as LSAuthToken,
18
+ )
19
+ from localstack_cli.pro.core.constants import VERSION
20
+ from localstack_cli.utils.functions import call_safe
21
+ from localstack_cli.utils.http import safe_requests
22
+ from localstack_cli.utils.json import FileMappedDocument
23
+ from localstack_cli.utils.strings import to_bytes, to_str
24
+
25
+ LOG = logging.getLogger(__name__)
26
+
27
+
28
+ AuthCache = FileMappedDocument | dict
29
+
30
+ # TODO: reconcile this module with localstack.bootstrap.licensingv2
31
+
32
+
33
+ class AuthToken:
34
+ """A thin wrapper around auth tokens, as not all of them a just strings but can also
35
+ include some other important details, like refresh token, expiration time etc."""
36
+
37
+ def __init__(self, token: str, metadata: dict | None = None):
38
+ self.token = token
39
+ self.metadata = metadata
40
+
41
+ def extract_jwt(self):
42
+ """Extract the JWT token from the given auth token (e.g., "Bearer <jwt_token>")."""
43
+ jwt_token = self.token.strip().split(" ")[-1]
44
+ # attempt to parse the token, then return it
45
+ jwt.decode(jwt_token, options={"verify_signature": False})
46
+ return jwt_token
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ return {**(self.metadata or {}), "token": self.token}
50
+
51
+
52
+ class AuthClient:
53
+ """Authentication client used to obtain auth tokens from the remote API endpoint"""
54
+
55
+ # token refresh lead time (secs before token expiry to initiate token refresh)
56
+ TOKEN_REFRESH_LEAD_TIME = 30
57
+
58
+ def get_auth_token(
59
+ self, username: str, password: str, headers: dict | None = None
60
+ ) -> AuthToken | None:
61
+ data = {"username": username, "password": password}
62
+ response = safe_requests.post(
63
+ self._api_url("/user/signin"), json.dumps(data), headers=headers
64
+ )
65
+ if not response.ok:
66
+ return None
67
+ try:
68
+ result = json.loads(to_str(response.content or "{}"))
69
+ return AuthToken(token=result["token"], metadata=result)
70
+ except Exception:
71
+ pass
72
+
73
+ def get_token_expiry(self, token: AuthToken) -> int | None:
74
+ """Get the expiry (epoch timestamp) of the given auth token, or None if this is not a valid (JWT) token"""
75
+ try:
76
+ # note: not performing token verification here, extracting expiry date directly
77
+ claims = jwt.decode(token.extract_jwt(), options={"verify_signature": False})
78
+ return claims.get("exp")
79
+ except jwt.PyJWTError:
80
+ # looks like this is not a JWT token (e.g., when using legacy auth tokens) -> return
81
+ return
82
+
83
+ def refresh_token(self, token: AuthToken) -> AuthToken:
84
+ """Check the expiry of the given token, and perform token refresh if it is (about to) expire(d)"""
85
+ expiry = self.get_token_expiry(token)
86
+ if not expiry or time.time() < expiry - self.TOKEN_REFRESH_LEAD_TIME:
87
+ return token
88
+
89
+ data = token.to_dict()
90
+ response = safe_requests.put(self._api_url("/user/signin"), json.dumps(data))
91
+ if not response.ok:
92
+ raise Exception(
93
+ f"Unable to obtain auth token (code {response.status_code}) - please log in again."
94
+ )
95
+ try:
96
+ result = json.loads(to_str(response.content or "{}"))
97
+ token_with_metadata = result["token"]
98
+ return AuthToken(token=token_with_metadata["token"], metadata=token_with_metadata)
99
+ except Exception as e:
100
+ raise Exception(f"Unable to obtain token ({e}) - please log in again.")
101
+
102
+ def read_credentials(
103
+ self, username: str | None = None, password: str | None = None
104
+ ) -> tuple[str, str, dict]:
105
+ # TODO: try to read from a configured credentials file in config.dirs.config
106
+ # if username and password is not specified, prompt for input
107
+ if not username or not password:
108
+ sys.stdout.write("Please provide your login credentials below\n")
109
+ sys.stdout.flush()
110
+ if not username:
111
+ sys.stdout.write("Username: ")
112
+ sys.stdout.flush()
113
+ username = input()
114
+ if not password:
115
+ password = getpass.getpass()
116
+ return username, password, {}
117
+
118
+ def _api_url(self, path: str) -> str:
119
+ return f"{API_ENDPOINT}{path}"
120
+
121
+
122
+ def get_auth_cache() -> FileMappedDocument:
123
+ """Auth cache for the host on the filesystem."""
124
+ return FileMappedDocument(pro_config.AUTH_CACHE_PATH, mode=0o600)
125
+
126
+
127
+ def login(username: str | None = None, password: str | None = None) -> None:
128
+ """
129
+ Authenticate against the /user/signin endpoint. If credentials are correct, it stores the token in the cache.
130
+ The bearer token can be used to sub-sequentially authenticate various calls to platform, e.g., Cloud Pod operations.
131
+ :param username: the username for the login.
132
+ :param password: the password for the login.
133
+ :raise an exception if the credentials are not valid.
134
+ """
135
+ auth_client = AuthClient()
136
+ username, password, headers = auth_client.read_credentials(username, password)
137
+ print("Verifying credentials ... (this may take a few moments)")
138
+ token = auth_client.get_auth_token(username, password, headers)
139
+ if not token:
140
+ raise Exception("Unable to verify login credentials - please try again")
141
+
142
+ # cache generated token
143
+ cache = get_auth_cache()
144
+ cache.update({"username": username, "token": token.token})
145
+ # a permission error can occur if the cache directory was created previously by a container
146
+ call_safe(cache.save, exception_message="error saving authentication information")
147
+
148
+
149
+ def logout() -> None:
150
+ """A logout operation clears the username and the token from the cache."""
151
+ cache = get_auth_cache()
152
+ cache.pop("username", None)
153
+ cache.pop("token", None)
154
+ cache.save()
155
+
156
+
157
+ def get_bearer_token_from_cache(auth_cache: AuthCache | None = None) -> dict[str, str]:
158
+ """
159
+ A successful login CLI command returns a bearer token that is stored into the auth_cache and can be used to make
160
+ authenticated calls against platform (e.g., for Cloud Pods).
161
+ This function retrieves such a token and build the authorization header for such platform calls.
162
+ :param auth_cache: the cache for the token.
163
+ :return: the authorization header to authenticate against platform calls.
164
+ """
165
+ auth_cache = auth_cache or get_auth_cache()
166
+ token_dict = auth_cache.get("token")
167
+ id_token = token_dict
168
+ if isinstance(id_token, dict):
169
+ # refresh the auth token, if necessary
170
+ auth_client = AuthClient()
171
+ token_obj = AuthToken(token_dict.get("token"), metadata=token_dict)
172
+ token_obj = auth_client.refresh_token(token_obj)
173
+
174
+ # update token cache
175
+ token_dict_new = token_obj.to_dict()
176
+ if token_dict != token_dict_new:
177
+ token_dict.update(token_dict_new)
178
+ auth_cache["token"] = token_dict
179
+ auth_cache.save()
180
+
181
+ # get id token string from token object
182
+ id_token = token_obj.token
183
+
184
+ if id_token:
185
+ provider = auth_cache.get("provider") or "internal"
186
+ prefix = f"{provider} "
187
+ if not id_token.startswith(prefix) and " " not in id_token:
188
+ id_token = f"{prefix}{id_token}"
189
+ return {"authorization": id_token}
190
+
191
+
192
+ def get_platform_auth_headers(auth_cache: AuthCache | None = None) -> dict[str, str]:
193
+ """
194
+ Creates auth headers used for connecting to the LocalStack backend (a.k.a., our platform), either a
195
+ token bearer authorization header from the auth cache (created by a login command),
196
+ or the environment API key.
197
+ """
198
+ # Try to build the authentication headers from the credentials in the environment
199
+ credentials = get_credentials_from_environment()
200
+ if isinstance(credentials, LSAuthToken):
201
+ auth_token_encoded = to_str(base64.b64encode(to_bytes(f":{credentials.encoded()}")))
202
+ return {"Authorization": f"Basic {auth_token_encoded}"}
203
+ if isinstance(credentials, ApiKeyCredentials):
204
+ return {"ls-api-key": credentials.encoded(), "ls-version": VERSION}
205
+
206
+ # Try to retrieve the token from the auth cache
207
+ if not (auth := get_bearer_token_from_cache(auth_cache or get_auth_cache())):
208
+ raise Exception(
209
+ "Auth token not configured! "
210
+ "Please run 'localstack auth set-token <AUTH_TOKEN>', "
211
+ "or set the environment variable LOCALSTACK_AUTH_TOKEN to a valid auth token."
212
+ )
213
+ return auth
@@ -0,0 +1,55 @@
1
+ import logging
2
+
3
+ LOG = logging.getLogger(__name__)
4
+
5
+
6
+ def configure_systemd(revert: bool):
7
+ from subprocess import CalledProcessError
8
+
9
+ from localstack_cli import config
10
+ from localstack_cli.utils.container_utils.container_client import ContainerException
11
+ from localstack_cli.utils.docker_utils import DOCKER_CLIENT
12
+ from localstack_cli.utils.run import is_linux, run, to_str
13
+
14
+ if not is_linux():
15
+ LOG.warning("Command only supported on GNU/Linux")
16
+
17
+ try:
18
+ container_ip = DOCKER_CLIENT.get_container_ip(config.MAIN_CONTAINER_NAME)
19
+ container_network = DOCKER_CLIENT.get_networks(config.MAIN_CONTAINER_NAME)
20
+ container_network = container_network[0]
21
+ network_inspect = DOCKER_CLIENT.inspect_network(container_network)
22
+ network_interface = network_inspect["Options"].get("com.docker.network.bridge.name")
23
+ bridge_id = network_inspect["Id"][:12]
24
+ network_interface = network_interface or f"br-{bridge_id}"
25
+
26
+ if revert:
27
+ cmd = ["sudo", "resolvectl", "revert", network_interface]
28
+ run(cmd, shell=False, print_error=False)
29
+ LOG.info("Reverting DNS config completed!")
30
+ else:
31
+ cmd = ["sudo", "resolvectl", "dns", network_interface, container_ip]
32
+ run(cmd, shell=False, print_error=False)
33
+ cmd = [
34
+ "sudo",
35
+ "resolvectl",
36
+ "domain",
37
+ network_interface,
38
+ "~amazonaws.com",
39
+ "~aws.amazon.com",
40
+ "~cloudfront.net",
41
+ "~localhost.localstack.cloud",
42
+ ]
43
+ run(cmd, shell=False, print_error=False)
44
+ LOG.info("Setting DNS config completed!")
45
+
46
+ except ContainerException:
47
+ if config.DEBUG:
48
+ LOG.exception("Error while getting container information")
49
+ LOG.warning(
50
+ "LocalStack container might not be running. Is container %s running?",
51
+ config.MAIN_CONTAINER_NAME,
52
+ )
53
+ LOG.warning("Is $MAIN_CONTAINER_NAME set correctly?")
54
+ except CalledProcessError as e:
55
+ LOG.warning("Error while configuring systemd-resolved: %s", to_str(e.output).strip())