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,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())
|