localstack 4.13.2.dev3__py3-none-any.whl → 4.13.2.dev53__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-4.13.2.dev3.dist-info → localstack-4.13.2.dev53.dist-info}/METADATA +2 -2
- {localstack-4.13.2.dev3.dist-info → localstack-4.13.2.dev53.dist-info}/RECORD +16 -16
- localstack-4.13.2.dev53.dist-info/entry_points.txt +17 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +4 -143
- localstack_cli/pro/core/plugins.py +81 -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/analytics/metadata.py +14 -20
- localstack_cli/utils/bootstrap.py +3 -8
- localstack_cli/version.py +2 -2
- localstack-4.13.2.dev3.dist-info/entry_points.txt +0 -6
- localstack_cli/pro/core/bootstrap/decryption.py +0 -160
- localstack_cli/utils/analytics/metrics/__init__.py +0 -6
- localstack_cli/utils/analytics/metrics/api.py +0 -58
- localstack_cli/utils/analytics/metrics/counter.py +0 -212
- localstack_cli/utils/analytics/metrics/publisher.py +0 -45
- localstack_cli/utils/analytics/metrics/registry.py +0 -97
- localstack_cli/utils/analytics/service_providers.py +0 -22
- {localstack-4.13.2.dev3.dist-info → localstack-4.13.2.dev53.dist-info}/WHEEL +0 -0
- {localstack-4.13.2.dev3.dist-info → localstack-4.13.2.dev53.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localstack
|
|
3
|
-
Version: 4.13.2.
|
|
3
|
+
Version: 4.13.2.dev53
|
|
4
4
|
Summary: The LocalStack Command Line Interface
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -24,7 +24,7 @@ Requires-Dist: cryptography
|
|
|
24
24
|
Requires-Dist: dnslib>=0.9.10
|
|
25
25
|
Requires-Dist: dnspython>=1.16.0
|
|
26
26
|
Requires-Dist: docker>=6.1.1
|
|
27
|
-
Requires-Dist: plux>=1.
|
|
27
|
+
Requires-Dist: plux>=1.14.0
|
|
28
28
|
Requires-Dist: psutil>=5.4.8
|
|
29
29
|
Requires-Dist: python-dotenv>=0.19.1
|
|
30
30
|
Requires-Dist: pyyaml>=5.1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
localstack_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
localstack_cli/config.py,sha256=kjt-tEH5TG3EPtHReLr23DzAw5XI_hZF5d8GEzCfuns,67164
|
|
3
3
|
localstack_cli/constants.py,sha256=Z65z7evTRQGRf1buYMMvLwKZOI0XfzlUsNmeUGcONzQ,6304
|
|
4
|
-
localstack_cli/version.py,sha256=
|
|
4
|
+
localstack_cli/version.py,sha256=3I42U3YyhfokzXbjISyosAx6wqRUCvQ9vOovMDUic5U,721
|
|
5
5
|
localstack_cli/cli/__init__.py,sha256=S0u4eNluwhhLkiCC8UZNjhh27Pk-W7jeUiEv4k3VrXo,176
|
|
6
6
|
localstack_cli/cli/console.py,sha256=eMz2PfQRDG493uzXFqQZTbYTC4NhO6UOpx4ZricsGLI,346
|
|
7
7
|
localstack_cli/cli/core_plugin.py,sha256=bVbkbhIOegsfQvD3BF_T477K6v6yjK0lRn4eE5PpnSI,382
|
|
@@ -22,16 +22,17 @@ localstack_cli/pro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
22
22
|
localstack_cli/pro/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
localstack_cli/pro/core/config.py,sha256=hyf7eKNfeYnH3jeKyRUV-IGJI2oWnx0o9_xzjHueYkc,24822
|
|
24
24
|
localstack_cli/pro/core/constants.py,sha256=xlaC6U4ZeAqMmAhOcVPsQyXTND-rt4k20tAxSc7m_Qk,1780
|
|
25
|
+
localstack_cli/pro/core/plugins.py,sha256=AoQO5XbG2JpmeFQ6q1QCphMV_uzuwPFZnmhlaKC2bac,3252
|
|
25
26
|
localstack_cli/pro/core/bootstrap/__init__.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329NoCACI,2
|
|
26
27
|
localstack_cli/pro/core/bootstrap/auth.py,sha256=jl2jkaAR6pFpGPXFLQIB_o2I8BHaA_h199McIk7gdIk,8800
|
|
27
|
-
localstack_cli/pro/core/bootstrap/decryption.py,sha256=4TygJZz3E1fV6HskmdNqUaaCQh8cWAKAVbSYAGd9MRQ,5894
|
|
28
28
|
localstack_cli/pro/core/bootstrap/dns_utils.py,sha256=qEUKKGbWK2iDDOaKND91fz3saYODDQxNk8LLAe5KEp8,2207
|
|
29
29
|
localstack_cli/pro/core/bootstrap/entitlements.py,sha256=zU84AANqZVCKZ0_yXFRLJaUBkN_hV9xlYMsV3QDEJvw,3757
|
|
30
|
-
localstack_cli/pro/core/bootstrap/licensingv2.py,sha256=
|
|
30
|
+
localstack_cli/pro/core/bootstrap/licensingv2.py,sha256=OMmti11WHtmB6HbyK3zG1XhISU5iTk35K9tRFmhUeZY,46216
|
|
31
31
|
localstack_cli/pro/core/bootstrap/pods_client.py,sha256=l6D1eXHoCWW-jfbB0LVdT4H0Mg_EuYoftLQTVyEBD6I,33263
|
|
32
32
|
localstack_cli/pro/core/bootstrap/extensions/__init__.py,sha256=m1UNp2aCa8M_mCdU1KhvfAWldfeLMPSM7ssyxkRYfKo,65
|
|
33
33
|
localstack_cli/pro/core/bootstrap/extensions/__main__.py,sha256=hBnA5G6RkVF7iNQCPT4F0befFLwvJAYUdThZ7oNmJEI,3000
|
|
34
34
|
localstack_cli/pro/core/bootstrap/extensions/autoinstall.py,sha256=90Y_vxD3ogU3wUDnj3sAiuU0Ot4KSpTG9Gp2wXZKfqY,1825
|
|
35
|
+
localstack_cli/pro/core/bootstrap/extensions/bootstrap.py,sha256=iFKTYDTpZAZVHSz2W2fPXTYRNoJP-XTc_to4Ki0GS_I,3412
|
|
35
36
|
localstack_cli/pro/core/bootstrap/extensions/repository.py,sha256=OmriRzU1JvWYgaqr1d0Rsrq-tJSOS1goPZ0KwPR65I8,12267
|
|
36
37
|
localstack_cli/pro/core/bootstrap/pods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
localstack_cli/pro/core/bootstrap/pods/api_types.py,sha256=yFuiRB6Hau52QHLt9eGjJRg-Rp9S_nAkBCvvcekufZo,950
|
|
@@ -55,10 +56,15 @@ localstack_cli/pro/core/cli/localstack.py,sha256=Z3_7W7UgUGoNgh3m9NS6JjX9nFU1o9O
|
|
|
55
56
|
localstack_cli/pro/core/cli/replicator.py,sha256=sreAt9hSSDyAHj2wLM2EYClVTLK97pWybAiAu4_veuA,11189
|
|
56
57
|
localstack_cli/pro/core/cli/state.py,sha256=iS8_qSqoxUlQj21nrVToazqjfM7PfBs-3RqoiwpIKn0,6696
|
|
57
58
|
localstack_cli/pro/core/cli/tree_view.py,sha256=K443WkwIxNRDi_rzPiQW5wBTq3dCgzfTiGbVNpeIJ68,7254
|
|
59
|
+
localstack_cli/runtime/__init__.py,sha256=sdfCHRYcw9uvUZsUNzRPh1vEh9kx8w1v1wgOORYnfZs,231
|
|
60
|
+
localstack_cli/runtime/exceptions.py,sha256=qDUqgzcBIxKP8ts0MlGJXskK5w1QWnOyuuZu1yUPzjM,228
|
|
61
|
+
localstack_cli/runtime/hooks.py,sha256=peE2r7BtbVNiiI_YcrDzVZm7CnzVER_EybcZsv0HBh8,2288
|
|
62
|
+
localstack_cli/testing/__init__.py,sha256=2Bk9bgTCK00hAmxbIuHCt7x50RRB9q_ciciIwCpPfGQ,48
|
|
63
|
+
localstack_cli/testing/config.py,sha256=xDm8m4iizy3C2bhZ9Ls-XV9RQcDhHW-33f8Yimj6GEM,151
|
|
58
64
|
localstack_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
65
|
localstack_cli/utils/archives.py,sha256=JHiMWmVhxxEruylj1LoUwJdruQmGbLPNORPTlTFBwek,9825
|
|
60
66
|
localstack_cli/utils/batching.py,sha256=pPRrbb9Ty5pHUMQHjS0tagIxBMPVUdzDaZz-iZPk8Is,8636
|
|
61
|
-
localstack_cli/utils/bootstrap.py,sha256=
|
|
67
|
+
localstack_cli/utils/bootstrap.py,sha256=AQ2TD2ZQYeNwBopRjlRj7IKkDSD6HPNjMuxvA_mFAOI,51481
|
|
62
68
|
localstack_cli/utils/checksum.py,sha256=Lt19VzwsUYEvPqCsQR8_RoeekOklDYX5aY8A2z9udAw,9730
|
|
63
69
|
localstack_cli/utils/collections.py,sha256=EVP_HRN7MHSrPmNqWSjMnoZ8T2yeX09M3ROqf84YAHM,18515
|
|
64
70
|
localstack_cli/utils/common.py,sha256=zxmwRRpLEqcHb3iSqcxnvK2eeU9QZGuraXVumzaf9aU,6503
|
|
@@ -90,23 +96,17 @@ localstack_cli/utils/analytics/cli.py,sha256=FXN-MduXf3H-VJU0rPIuxiUHRw2qktcQRHs
|
|
|
90
96
|
localstack_cli/utils/analytics/client.py,sha256=ct6DS6yF1E-zwS_aAik5jEZmDoaUChChuf26VklPPzw,3425
|
|
91
97
|
localstack_cli/utils/analytics/events.py,sha256=_YHZf_7nKluUi_ZZfvI4QId3onXKisuAXGISugbzNeQ,563
|
|
92
98
|
localstack_cli/utils/analytics/logger.py,sha256=ufQ-M9G_MyhZrh9Ech1MJnKMlRBormZOh4ujs0FfdA0,1401
|
|
93
|
-
localstack_cli/utils/analytics/metadata.py,sha256=
|
|
99
|
+
localstack_cli/utils/analytics/metadata.py,sha256=RMbiRLnEKTTsF-hzoWK372it_WbB1UD7JRrQBVO0skA,7311
|
|
94
100
|
localstack_cli/utils/analytics/publisher.py,sha256=kcQLuZlWsg7aNCOj7rXk5MXT8J8JskFQDOYnSr1_3d8,5032
|
|
95
|
-
localstack_cli/utils/analytics/service_providers.py,sha256=9ZrapP28KyY8uzAAwl_H87KicNGTo9-ZD0ddQnNHUx0,786
|
|
96
101
|
localstack_cli/utils/analytics/service_request_aggregator.py,sha256=bdRX-SSv66_wipmCTtBrJREOzCHBG_zKoj0EmUd0rwg,4112
|
|
97
|
-
localstack_cli/utils/analytics/metrics/__init__.py,sha256=APJa7ulgGseR4dl4EPAhKOEuXxxNiP0f_MwLbNmgc4Y,233
|
|
98
|
-
localstack_cli/utils/analytics/metrics/api.py,sha256=5TfPqbvfgikMhQYSoT5hOaxTpNUIJo2iQYFOb0e0DgQ,1595
|
|
99
|
-
localstack_cli/utils/analytics/metrics/counter.py,sha256=LgmY5p0zQJ5CqR-V_QyqOOWkquKFSq9UJLu0JsZtqqw,6834
|
|
100
|
-
localstack_cli/utils/analytics/metrics/publisher.py,sha256=aQ4ny9SYWNff2XHU6PD0_fNzPHtijNJYhdfypiasS9M,1352
|
|
101
|
-
localstack_cli/utils/analytics/metrics/registry.py,sha256=LMMGNlxYX3I3IeRC2-84B2ehlqA2EBb4Ic1ZP8-n8y4,2822
|
|
102
102
|
localstack_cli/utils/container_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
103
103
|
localstack_cli/utils/container_utils/container_client.py,sha256=EjCAIP6WPuI8w01iECo3VuoHzkcpLhz9VlJsPFJ-msI,59629
|
|
104
104
|
localstack_cli/utils/container_utils/docker_cmd_client.py,sha256=4UPKOZTyu2TgPlEc5JpNgm1hvNcT5cp7CEmZjX9Hu2I,40086
|
|
105
105
|
localstack_cli/utils/container_utils/docker_sdk_client.py,sha256=6lnBhjT03otVGBm15WDq3LAfZnxLc4Q1lyjHAyFHsbM,40460
|
|
106
106
|
localstack_cli/utils/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
107
107
|
localstack_cli/utils/server/tcp_proxy.py,sha256=0ehOD5muo2cBCg0boXzKhzF14KfwlJ7VUtM5i8X7M5A,4006
|
|
108
|
-
localstack-4.13.2.
|
|
109
|
-
localstack-4.13.2.
|
|
110
|
-
localstack-4.13.2.
|
|
111
|
-
localstack-4.13.2.
|
|
112
|
-
localstack-4.13.2.
|
|
108
|
+
localstack-4.13.2.dev53.dist-info/METADATA,sha256=W6nIj7w2PCotu0C2VWaC_MTjEM7MxKS8ex6shusRQok,2709
|
|
109
|
+
localstack-4.13.2.dev53.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
110
|
+
localstack-4.13.2.dev53.dist-info/entry_points.txt,sha256=ogxbxjXC9xK1eksf9CUvOEl09SY2qpUt19SVya0WYAY,933
|
|
111
|
+
localstack-4.13.2.dev53.dist-info/top_level.txt,sha256=02blzxjv7TnyriE5gkWIM_UqKTSBiCFPgRG5ZBO5R0Y,15
|
|
112
|
+
localstack-4.13.2.dev53.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
localstack = localstack_cli.cli.main:main
|
|
3
|
+
|
|
4
|
+
[localstack_cli.hooks.configure_localstack_container]
|
|
5
|
+
configure_extensions_dev_container = localstack_cli.pro.core.plugins:configure_extensions_dev_container
|
|
6
|
+
configure_pro_container = localstack_cli.pro.core.plugins:configure_pro_container
|
|
7
|
+
set_analytics_header = localstack_cli.utils.analytics.metadata:set_analytics_header
|
|
8
|
+
|
|
9
|
+
[localstack_cli.hooks.prepare_host]
|
|
10
|
+
activate_pro_key_on_host = localstack_cli.pro.core.plugins:activate_pro_key_on_host
|
|
11
|
+
configure_extensions_dev_host = localstack_cli.pro.core.plugins:configure_extensions_dev_host
|
|
12
|
+
patch_community_pro_detection = localstack_cli.pro.core.plugins:patch_community_pro_detection
|
|
13
|
+
publish_invocation_metadata = localstack_cli.utils.analytics.metadata:publish_invocation_metadata
|
|
14
|
+
|
|
15
|
+
[localstack_cli.plugins.cli]
|
|
16
|
+
core = localstack_cli.cli.core_plugin:CoreCliPlugin
|
|
17
|
+
pro = localstack_cli.pro.core.cli.localstack:ProCliPlugins
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Hooks for extension developer mode on the host CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import gettempdir
|
|
9
|
+
|
|
10
|
+
from localstack_cli import config
|
|
11
|
+
from localstack_cli.utils.bootstrap import Container
|
|
12
|
+
from localstack_cli.utils.container_utils.container_client import BindMount
|
|
13
|
+
from localstack_cli.utils.json import FileMappedDocument
|
|
14
|
+
from localstack_cli.utils.strings import md5
|
|
15
|
+
|
|
16
|
+
LOG = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_ENTRYPOINT_SCRIPT = """#!/bin/bash
|
|
19
|
+
echo "=================================================="
|
|
20
|
+
echo "LocalStack extension developer mode enabled"
|
|
21
|
+
shopt -s nullglob
|
|
22
|
+
|
|
23
|
+
pkgs=$(echo /opt/code/localstack/.venv/lib/python3*/site-packages)
|
|
24
|
+
|
|
25
|
+
echo "import localstack_extensions_resolve;localstack_extensions_resolve.resolve()" > ${pkgs}/localstack-extensions-resolve.pth
|
|
26
|
+
|
|
27
|
+
cat << EOF >> ${pkgs}/localstack_extensions_resolve.py
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
import glob
|
|
31
|
+
|
|
32
|
+
base_dirs_visited = set()
|
|
33
|
+
|
|
34
|
+
def append_pth_recursively(base_dir):
|
|
35
|
+
if base_dir in base_dirs_visited:
|
|
36
|
+
return
|
|
37
|
+
base_dirs_visited.add(base_dir)
|
|
38
|
+
|
|
39
|
+
for f in glob.glob(f"{base_dir}/*.pth", recursive=True):
|
|
40
|
+
with open(f, "r") as fd:
|
|
41
|
+
abs_path = os.path.abspath(os.path.dirname(f))
|
|
42
|
+
lines = fd.readlines()
|
|
43
|
+
for line in lines:
|
|
44
|
+
if line := line.strip():
|
|
45
|
+
if "import" in line:
|
|
46
|
+
continue
|
|
47
|
+
module_dir = os.path.abspath(os.path.join(abs_path, line)) if not os.path.isabs(line) else line
|
|
48
|
+
if os.path.exists(module_dir) and os.path.isdir(module_dir):
|
|
49
|
+
if module_dir not in sys.path:
|
|
50
|
+
sys.path.append(module_dir)
|
|
51
|
+
append_pth_recursively(module_dir)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve():
|
|
55
|
+
append_pth_recursively(os.path.dirname(__file__))
|
|
56
|
+
EOF
|
|
57
|
+
|
|
58
|
+
for d in /opt/code/extensions/* ;
|
|
59
|
+
do
|
|
60
|
+
echo "- mounting extension ${d}"
|
|
61
|
+
find ${d} -type d -name "site-packages" >> ${pkgs}/localstack-extensions-venv.pth
|
|
62
|
+
echo ${d} >> ${pkgs}/localstack-extensions-venv.pth
|
|
63
|
+
done
|
|
64
|
+
echo "Resuming normal execution, ..."
|
|
65
|
+
echo "=================================================="
|
|
66
|
+
exec /usr/local/bin/docker-entrypoint.sh
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_host_extension_dirs: list[str] = []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_on_configure_host_hook():
|
|
73
|
+
"""Load extension directories from ~/.localstack/extensions-dev.json."""
|
|
74
|
+
doc = FileMappedDocument(os.path.join(config.CONFIG_DIR, "extensions-dev.json"))
|
|
75
|
+
|
|
76
|
+
for extensions_spec in doc.get("extensions", []):
|
|
77
|
+
path = extensions_spec.get("host_path")
|
|
78
|
+
if path and os.path.exists(path):
|
|
79
|
+
_host_extension_dirs.append(path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_on_configure_localstack_container_hook(container: Container):
|
|
83
|
+
"""Configure container for extension dev mode."""
|
|
84
|
+
# Create and mount custom entrypoint script
|
|
85
|
+
h = md5(_ENTRYPOINT_SCRIPT)
|
|
86
|
+
file = Path(gettempdir(), f"docker-entrypoint-{h}.sh")
|
|
87
|
+
if not file.exists():
|
|
88
|
+
file.write_text(_ENTRYPOINT_SCRIPT, newline="\n", encoding="utf-8")
|
|
89
|
+
file.chmod(0o777)
|
|
90
|
+
|
|
91
|
+
container.config.volumes.add(BindMount(str(file), f"/tmp/{file.name}"))
|
|
92
|
+
container.config.entrypoint = f"/tmp/{file.name}"
|
|
93
|
+
|
|
94
|
+
# Mount extension directories
|
|
95
|
+
for ext_dir in _host_extension_dirs:
|
|
96
|
+
target = os.path.join("/opt/code/extensions", os.path.basename(ext_dir))
|
|
97
|
+
container.config.volumes.add(BindMount(ext_dir, target, read_only=True))
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import binascii
|
|
3
2
|
import copy
|
|
4
3
|
import dataclasses
|
|
5
|
-
import hashlib
|
|
6
4
|
import hmac
|
|
7
5
|
import json
|
|
8
6
|
import logging
|
|
@@ -26,9 +24,8 @@ from localstack_cli.pro.core.bootstrap.entitlements import (
|
|
|
26
24
|
ProductInfo,
|
|
27
25
|
)
|
|
28
26
|
from localstack_cli.pro.core.constants import PLATFORM_PLUGIN_NAMESPACE
|
|
29
|
-
from localstack_cli.utils.files import load_file
|
|
30
27
|
from localstack_cli.utils.objects import singleton_factory
|
|
31
|
-
from localstack_cli.utils.strings import md5
|
|
28
|
+
from localstack_cli.utils.strings import md5
|
|
32
29
|
from plux import Plugin, PluginDisabled, PluginLifecycleListener, PluginSpec
|
|
33
30
|
|
|
34
31
|
LOG = logging.getLogger(__name__)
|
|
@@ -446,20 +443,6 @@ class LicensingClient:
|
|
|
446
443
|
"""
|
|
447
444
|
raise NotImplementedError
|
|
448
445
|
|
|
449
|
-
def decode_decryption_key(self, credentials: Credentials, license: License) -> bytes:
|
|
450
|
-
raise NotImplementedError
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
class LicenseSecretDecoder:
|
|
454
|
-
"""
|
|
455
|
-
Class to decode the ``license_secret`` field from the given license. The field contains a secret which is
|
|
456
|
-
the decryption key for the localstack code.
|
|
457
|
-
"""
|
|
458
|
-
|
|
459
|
-
def decode_license_secret(self, license: LicenseV1) -> bytes: # noqa
|
|
460
|
-
raise NotImplementedError
|
|
461
|
-
|
|
462
|
-
|
|
463
446
|
#####################
|
|
464
447
|
# v1 implementation #
|
|
465
448
|
#####################
|
|
@@ -518,64 +501,6 @@ class LicenseParser:
|
|
|
518
501
|
raise LicenseFormatError(f"error parsing license file: {e}") from e
|
|
519
502
|
|
|
520
503
|
|
|
521
|
-
class AESLicenseV1SecretDecoder(LicenseSecretDecoder):
|
|
522
|
-
"""
|
|
523
|
-
Uses a custom AES encryption scheme to decode the source decryption key from the license key. This is
|
|
524
|
-
mostly obfuscation and not really a trust mechanism.
|
|
525
|
-
|
|
526
|
-
The shared key is the sha256 digest of the licensing credentials concatenated with the license signature.
|
|
527
|
-
"""
|
|
528
|
-
|
|
529
|
-
scheme = "LS1.0"
|
|
530
|
-
|
|
531
|
-
def __init__(self, credentials: Credentials):
|
|
532
|
-
self.credentials = credentials
|
|
533
|
-
|
|
534
|
-
def decode_license_secret(self, license: LicenseV1) -> bytes: # noqa
|
|
535
|
-
if not license.signature:
|
|
536
|
-
raise LicenseFormatError("license is not signed")
|
|
537
|
-
|
|
538
|
-
# use the signature of the license as well as the credentials to encode the license key
|
|
539
|
-
hash_ = hashlib.sha256()
|
|
540
|
-
hash_.update(self.credentials.to_bytes())
|
|
541
|
-
hash_.update(binascii.unhexlify(license.signature)) # use the byte value, not the string
|
|
542
|
-
key = hash_.digest()
|
|
543
|
-
|
|
544
|
-
# extract the scheme from the base64 encoded string
|
|
545
|
-
license_secret = base64.b64decode(license.license_secret)
|
|
546
|
-
parts = license_secret.split(b":", maxsplit=1)
|
|
547
|
-
if len(parts) != 2: # noqa PLR2004
|
|
548
|
-
raise LicenseFormatError("invalid license key format")
|
|
549
|
-
scheme, secret = parts
|
|
550
|
-
|
|
551
|
-
if scheme.decode("utf-8") != self.scheme:
|
|
552
|
-
raise ValueError(f"unknown scheme {scheme}")
|
|
553
|
-
|
|
554
|
-
iv = secret[:16]
|
|
555
|
-
encrypted_data = secret[16:]
|
|
556
|
-
|
|
557
|
-
return self._aes_decrypt(key, iv, encrypted_data)
|
|
558
|
-
|
|
559
|
-
def _aes_decrypt(self, key: bytes, iv: bytes, encrypted_data: bytes):
|
|
560
|
-
from cryptography.hazmat.primitives import padding
|
|
561
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
562
|
-
|
|
563
|
-
# Create an AES cipher with the provided key and CBC mode
|
|
564
|
-
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
|
565
|
-
|
|
566
|
-
# Create a decryptor object using the AES cipher
|
|
567
|
-
decryptor = cipher.decryptor()
|
|
568
|
-
|
|
569
|
-
# Decrypt the encrypted data
|
|
570
|
-
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
571
|
-
|
|
572
|
-
# Remove PKCS7 padding from the decrypted data
|
|
573
|
-
unpadder = padding.PKCS7(128).unpadder()
|
|
574
|
-
unpadded_data = unpadder.update(decrypted_data)
|
|
575
|
-
|
|
576
|
-
return unpadded_data + unpadder.finalize()
|
|
577
|
-
|
|
578
|
-
|
|
579
504
|
class LicenseV1ClientBase(LicensingClient):
|
|
580
505
|
def validate_license(self, credentials: Credentials, license: LicenseV1):
|
|
581
506
|
try:
|
|
@@ -640,17 +565,6 @@ class LicenseV1Client(LicenseV1ClientBase):
|
|
|
640
565
|
license_.offline_data["localstack_version"] = VERSION
|
|
641
566
|
return license_
|
|
642
567
|
|
|
643
|
-
def decode_decryption_key(self, credentials: Credentials, license: LicenseV1) -> bytes:
|
|
644
|
-
key = base64.b64decode(license.license_secret)
|
|
645
|
-
parts = key.split(b":", maxsplit=1)
|
|
646
|
-
scheme = to_str(parts[0])
|
|
647
|
-
if scheme == AESLicenseV1SecretDecoder.scheme:
|
|
648
|
-
decoder = AESLicenseV1SecretDecoder(credentials)
|
|
649
|
-
else:
|
|
650
|
-
raise LicenseFormatError(f"unknown license key scheme {scheme}")
|
|
651
|
-
|
|
652
|
-
return decoder.decode_license_secret(license)
|
|
653
|
-
|
|
654
568
|
def _get_machine_data(self) -> dict:
|
|
655
569
|
from localstack_cli.utils.analytics.metadata import get_client_metadata
|
|
656
570
|
|
|
@@ -826,10 +740,9 @@ class LicensedLocalstackEnvironment:
|
|
|
826
740
|
|
|
827
741
|
def activate(self, offline_only: bool = False):
|
|
828
742
|
"""
|
|
829
|
-
This high-level method
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
activation and code decryption.
|
|
743
|
+
This high-level method activates the license using credentials from the environment.
|
|
744
|
+
Once completed, it sets ``self.license`` to the license document that was activated.
|
|
745
|
+
This method will also set the env var ``PRO_ACTIVATED=1`` after successful license activation.
|
|
833
746
|
|
|
834
747
|
See ``activate_license`` for the activation algorithm.
|
|
835
748
|
|
|
@@ -854,9 +767,6 @@ class LicensedLocalstackEnvironment:
|
|
|
854
767
|
|
|
855
768
|
self.activate_license(offline_only)
|
|
856
769
|
|
|
857
|
-
if not self.is_decryption_enabled():
|
|
858
|
-
self.enable_decryption()
|
|
859
|
-
|
|
860
770
|
os.environ[constants.ENV_PRO_ACTIVATED] = "1"
|
|
861
771
|
|
|
862
772
|
def activate_license(self, offline_only: bool = False):
|
|
@@ -974,55 +884,6 @@ class LicensedLocalstackEnvironment:
|
|
|
974
884
|
"""
|
|
975
885
|
return self.product_entitlements.has_entitlement(product_name)
|
|
976
886
|
|
|
977
|
-
def enable_decryption(self):
|
|
978
|
-
"""
|
|
979
|
-
Called by ``activate`` to read the source decryption key from the license key and register the
|
|
980
|
-
``DecryptionHandler`` into the sys path.
|
|
981
|
-
"""
|
|
982
|
-
if not self.activated or not self.license:
|
|
983
|
-
raise ValueError("license not yet activated")
|
|
984
|
-
|
|
985
|
-
from localstack_cli.pro.core.bootstrap.decryption import (
|
|
986
|
-
DecryptionHandler,
|
|
987
|
-
init_source_decryption,
|
|
988
|
-
)
|
|
989
|
-
from localstack_cli.pro.core.config import ROOT_FOLDER
|
|
990
|
-
|
|
991
|
-
decryption_handler = DecryptionHandler(
|
|
992
|
-
self.client.decode_decryption_key(self.require_valid_credentials(), self.license)
|
|
993
|
-
)
|
|
994
|
-
|
|
995
|
-
try:
|
|
996
|
-
file_name = f"{ROOT_FOLDER}/localstack/pro/core/utils/decryption_check.py.enc"
|
|
997
|
-
encrypted_file_content = load_file(file_name, mode="rb")
|
|
998
|
-
if not encrypted_file_content:
|
|
999
|
-
raise ValueError(
|
|
1000
|
-
"Decryption check file not found. Are you using localstack pro in host mode?"
|
|
1001
|
-
)
|
|
1002
|
-
file_content = decryption_handler.decrypt(encrypted_file_content)
|
|
1003
|
-
if b"decryption_check" not in file_content:
|
|
1004
|
-
raise ValueError("Decryption resulted in invalid python file")
|
|
1005
|
-
except Exception as e:
|
|
1006
|
-
raise LicenseActivationError(
|
|
1007
|
-
"Error while trying to perform code activation. You may be using a "
|
|
1008
|
-
"version of LocalStack that is not within your license agreement."
|
|
1009
|
-
) from e
|
|
1010
|
-
|
|
1011
|
-
init_source_decryption(decryption_handler)
|
|
1012
|
-
|
|
1013
|
-
def is_decryption_enabled(self) -> bool:
|
|
1014
|
-
"""
|
|
1015
|
-
Checks whether decryption is enabled. Decryption can be enabled in a test environment when the
|
|
1016
|
-
source code is available. So this can return true even if a license has not been activated.
|
|
1017
|
-
|
|
1018
|
-
:return: true if the normally encrypted source code can be accessed.
|
|
1019
|
-
"""
|
|
1020
|
-
try:
|
|
1021
|
-
from localstack_cli.pro.core.utils.decryption_check import decryption_check # noqa
|
|
1022
|
-
|
|
1023
|
-
return True
|
|
1024
|
-
except ImportError:
|
|
1025
|
-
return False
|
|
1026
887
|
|
|
1027
888
|
def save_license(self):
|
|
1028
889
|
"""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pro plugin hooks for the LocalStack CLI.
|
|
3
|
+
|
|
4
|
+
These hooks are executed on the host machine when running CLI commands like `localstack start`.
|
|
5
|
+
They handle license activation, container configuration, and extension developer mode.
|
|
6
|
+
|
|
7
|
+
Note: This file was extracted from localstack-pro-core/localstack/pro/core/plugins.py
|
|
8
|
+
and rewritten to contain only CLI-relevant functionality.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from localstack_cli import config as localstack_config
|
|
17
|
+
from localstack_cli.config import HostAndPort
|
|
18
|
+
from localstack_cli.pro.core import config as pro_config
|
|
19
|
+
from localstack_cli.pro.core.bootstrap import licensingv2
|
|
20
|
+
from localstack_cli.runtime import hooks
|
|
21
|
+
from localstack_cli.runtime.exceptions import LocalstackExit
|
|
22
|
+
from localstack_cli.utils.bootstrap import Container
|
|
23
|
+
|
|
24
|
+
LOG = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def modify_gateway_listen_config(cfg):
|
|
28
|
+
"""
|
|
29
|
+
Modifies the localstack config to additionally listen to port 443.
|
|
30
|
+
Needs to be called before any edge URLs are resolved using the config.
|
|
31
|
+
"""
|
|
32
|
+
if os.getenv("GATEWAY_LISTEN") is None:
|
|
33
|
+
host = "0.0.0.0" if localstack_config.in_docker() else "127.0.0.1"
|
|
34
|
+
cfg.GATEWAY_LISTEN.append(HostAndPort(host=host, port=443))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@hooks.prepare_host(priority=200)
|
|
38
|
+
def patch_community_pro_detection():
|
|
39
|
+
"""This is currently needed to make localstack core aware of the `localstack auth set-token`
|
|
40
|
+
functionality, where we set the key into the ``~/.localstack/auth.json`` file that community does not
|
|
41
|
+
yet know about. ``is_api_key_configured`` is used in the LocalStack CLI to determine whether to start
|
|
42
|
+
the localstack or localstack-pro container image."""
|
|
43
|
+
from localstack_cli.utils import bootstrap
|
|
44
|
+
|
|
45
|
+
bootstrap.is_auth_token_configured = pro_config.is_auth_token_configured
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@hooks.prepare_host(priority=100, should_load=pro_config.ACTIVATE_PRO)
|
|
49
|
+
def activate_pro_key_on_host():
|
|
50
|
+
"""Activate license on host (needed for DNS forward and EC2 daemon)."""
|
|
51
|
+
try:
|
|
52
|
+
licensingv2.get_licensed_environment().activate()
|
|
53
|
+
except licensingv2.LicensingError as e:
|
|
54
|
+
raise LocalstackExit(reason=e.get_user_friendly(), code=55)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@hooks.configure_localstack_container(priority=10, should_load=pro_config.ACTIVATE_PRO)
|
|
58
|
+
def configure_pro_container(container: Container):
|
|
59
|
+
"""Configure the LocalStack container for pro features."""
|
|
60
|
+
modify_gateway_listen_config(localstack_config)
|
|
61
|
+
container.configure(licensingv2.configure_container_licensing)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@hooks.prepare_host(should_load=pro_config.ACTIVATE_PRO and pro_config.EXTENSION_DEV_MODE)
|
|
65
|
+
def configure_extensions_dev_host():
|
|
66
|
+
"""Load extension directories from ~/.localstack/extensions-dev.json."""
|
|
67
|
+
from localstack_cli.pro.core.bootstrap.extensions.bootstrap import run_on_configure_host_hook
|
|
68
|
+
|
|
69
|
+
run_on_configure_host_hook()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@hooks.configure_localstack_container(
|
|
73
|
+
should_load=pro_config.ACTIVATE_PRO and pro_config.EXTENSION_DEV_MODE
|
|
74
|
+
)
|
|
75
|
+
def configure_extensions_dev_container(container: Container):
|
|
76
|
+
"""Configure container for extension developer mode."""
|
|
77
|
+
from localstack_cli.pro.core.bootstrap.extensions.bootstrap import (
|
|
78
|
+
run_on_configure_localstack_container_hook,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
run_on_configure_localstack_container_hook(container)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from plux import PluginManager, plugin
|
|
4
|
+
|
|
5
|
+
# plugin namespace constants
|
|
6
|
+
HOOKS_CONFIGURE_LOCALSTACK_CONTAINER = "localstack_cli.hooks.configure_localstack_container"
|
|
7
|
+
HOOKS_PREPARE_HOST = "localstack_cli.hooks.prepare_host"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hook(namespace: str, priority: int = 0, **kwargs):
|
|
11
|
+
"""
|
|
12
|
+
Decorator for creating functional plugins that have a hook_priority attribute. Hooks with a higher priority value
|
|
13
|
+
will be executed earlier.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def wrapper(fn):
|
|
17
|
+
fn.hook_priority = priority
|
|
18
|
+
return plugin(namespace=namespace, **kwargs)(fn)
|
|
19
|
+
|
|
20
|
+
return wrapper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def hook_spec(namespace: str):
|
|
24
|
+
"""
|
|
25
|
+
Creates a new hook decorator bound to a namespace.
|
|
26
|
+
|
|
27
|
+
on_infra_start = hook_spec("localstack.hooks.on_infra_start")
|
|
28
|
+
|
|
29
|
+
@on_infra_start()
|
|
30
|
+
def foo():
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# run all hooks in order
|
|
34
|
+
on_infra_start.run()
|
|
35
|
+
"""
|
|
36
|
+
fn = functools.partial(hook, namespace=namespace)
|
|
37
|
+
# attach hook manager and run method to decorator for convenience calls
|
|
38
|
+
fn.manager = HookManager(namespace)
|
|
39
|
+
fn.run = fn.manager.run_in_order
|
|
40
|
+
return fn
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HookManager(PluginManager):
|
|
44
|
+
def load_all_sorted(self, propagate_exceptions=False):
|
|
45
|
+
"""
|
|
46
|
+
Loads all hook plugins and sorts them by their hook_priority attribute.
|
|
47
|
+
"""
|
|
48
|
+
plugins = self.load_all(propagate_exceptions)
|
|
49
|
+
# the hook_priority attribute is part of the function wrapped in the FunctionPlugin
|
|
50
|
+
plugins.sort(
|
|
51
|
+
key=lambda _fn_plugin: getattr(_fn_plugin.fn, "hook_priority", 0), reverse=True
|
|
52
|
+
)
|
|
53
|
+
return plugins
|
|
54
|
+
|
|
55
|
+
def run_in_order(self, *args, **kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Loads and runs all plugins in order them with the given arguments.
|
|
58
|
+
"""
|
|
59
|
+
for fn_plugin in self.load_all_sorted():
|
|
60
|
+
fn_plugin(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"HookManager({self.namespace})"
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return self.__str__()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
configure_localstack_container = hook_spec(HOOKS_CONFIGURE_LOCALSTACK_CONTAINER)
|
|
70
|
+
"""Hooks to configure the LocalStack container before it starts. Executed on the host when invoking the CLI."""
|
|
71
|
+
|
|
72
|
+
prepare_host = hook_spec(HOOKS_PREPARE_HOST)
|
|
73
|
+
"""Hooks to prepare the host that's starting LocalStack. Executed on the host when invoking the CLI."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Testing utilities for the LocalStack CLI."""
|
|
@@ -5,11 +5,7 @@ import platform
|
|
|
5
5
|
|
|
6
6
|
from localstack_cli import config
|
|
7
7
|
from localstack_cli.constants import VERSION
|
|
8
|
-
|
|
9
|
-
from localstack_cli.runtime import get_current_runtime, hooks
|
|
10
|
-
except ImportError:
|
|
11
|
-
get_current_runtime = None
|
|
12
|
-
hooks = None
|
|
8
|
+
from localstack_cli.runtime import get_current_runtime, hooks
|
|
13
9
|
from localstack_cli.utils.bootstrap import Container
|
|
14
10
|
from localstack_cli.utils.files import rm_rf
|
|
15
11
|
from localstack_cli.utils.functions import call_safe
|
|
@@ -237,20 +233,18 @@ def get_system() -> str:
|
|
|
237
233
|
return platform.system().lower()
|
|
238
234
|
|
|
239
235
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# lazy-init machine ID into cache on the host, which can then be used in the container
|
|
245
|
-
get_machine_id()
|
|
236
|
+
@hooks.prepare_host()
|
|
237
|
+
def publish_invocation_metadata():
|
|
238
|
+
"""Lazy-init machine ID into cache on the host, which can then be used in the container."""
|
|
239
|
+
get_machine_id()
|
|
246
240
|
|
|
247
|
-
@hooks.configure_localstack_container()
|
|
248
|
-
def _mount_machine_file(container: Container):
|
|
249
|
-
from localstack_cli.utils.container_utils.container_client import BindMount
|
|
250
241
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
242
|
+
@hooks.configure_localstack_container()
|
|
243
|
+
def set_analytics_header(container: Container):
|
|
244
|
+
"""Mount the machine file from the host's CLI cache directory into the container."""
|
|
245
|
+
from localstack_cli.utils.container_utils.container_client import BindMount
|
|
246
|
+
|
|
247
|
+
machine_file = os.path.join(config.dirs.cache, "machine.json")
|
|
248
|
+
if os.path.isfile(machine_file):
|
|
249
|
+
target = os.path.join(config.dirs.for_container().cache, "machine.json")
|
|
250
|
+
container.config.volumes.add(BindMount(machine_file, target, read_only=True))
|
|
@@ -21,10 +21,7 @@ from localstack_cli.config import (
|
|
|
21
21
|
load_environment,
|
|
22
22
|
)
|
|
23
23
|
from localstack_cli.constants import VERSION
|
|
24
|
-
|
|
25
|
-
from localstack_cli.runtime import hooks
|
|
26
|
-
except ImportError:
|
|
27
|
-
hooks = None # Runtime hooks not available in standalone CLI
|
|
24
|
+
from localstack_cli.runtime import hooks
|
|
28
25
|
from localstack_cli.utils.container_networking import get_main_container_name
|
|
29
26
|
from localstack_cli.utils.container_utils.container_client import (
|
|
30
27
|
BindMount,
|
|
@@ -1218,8 +1215,7 @@ def configure_container(container: Container):
|
|
|
1218
1215
|
container.config.additional_flags = f"{container.config.additional_flags} {user_flags}"
|
|
1219
1216
|
|
|
1220
1217
|
# get additional parameters from plux
|
|
1221
|
-
|
|
1222
|
-
hooks.configure_localstack_container.run(container)
|
|
1218
|
+
hooks.configure_localstack_container.run(container)
|
|
1223
1219
|
|
|
1224
1220
|
if config.DEVELOP:
|
|
1225
1221
|
container.config.ports.add(config.DEVELOP_PORT)
|
|
@@ -1256,8 +1252,7 @@ def prepare_host(console):
|
|
|
1256
1252
|
console.print_exception()
|
|
1257
1253
|
|
|
1258
1254
|
setup_logging()
|
|
1259
|
-
|
|
1260
|
-
hooks.prepare_host.run()
|
|
1255
|
+
hooks.prepare_host.run()
|
|
1261
1256
|
|
|
1262
1257
|
|
|
1263
1258
|
def start_infra_in_docker(console, cli_params: dict[str, Any] = None):
|
localstack_cli/version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '4.13.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (4, 13, 2, '
|
|
31
|
+
__version__ = version = '4.13.2.dev53'
|
|
32
|
+
__version_tuple__ = version_tuple = (4, 13, 2, 'dev53')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import os.path
|
|
3
|
-
import sys
|
|
4
|
-
import tempfile
|
|
5
|
-
import tokenize
|
|
6
|
-
import traceback
|
|
7
|
-
from importlib.abc import MetaPathFinder, SourceLoader
|
|
8
|
-
from importlib.util import spec_from_file_location
|
|
9
|
-
|
|
10
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
11
|
-
from localstack_cli.utils.files import load_file
|
|
12
|
-
from localstack_cli.utils.patch import patch
|
|
13
|
-
from localstack_cli.utils.strings import to_str
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class DecryptionHandler:
|
|
17
|
-
decryption_key: bytes
|
|
18
|
-
|
|
19
|
-
def __init__(self, decryption_key: bytes):
|
|
20
|
-
self.decryption_key = decryption_key
|
|
21
|
-
|
|
22
|
-
def decrypt(self, content) -> bytes:
|
|
23
|
-
iv = b"\0" * 16
|
|
24
|
-
cipher = Cipher(algorithms.AES(self.decryption_key), modes.CBC(iv))
|
|
25
|
-
decryptor = cipher.decryptor()
|
|
26
|
-
|
|
27
|
-
decrypted = decryptor.update(content) + decryptor.finalize()
|
|
28
|
-
# Remove padding bytes
|
|
29
|
-
decrypted = decrypted.partition(b"\0")[0]
|
|
30
|
-
return decrypted
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class EncryptedFileFinder(MetaPathFinder):
|
|
34
|
-
decryption_handler: DecryptionHandler
|
|
35
|
-
|
|
36
|
-
def __init__(self, decryption_handler: DecryptionHandler):
|
|
37
|
-
self.decryption_handler = decryption_handler
|
|
38
|
-
|
|
39
|
-
def find_spec(self, fullname, path, target=None):
|
|
40
|
-
# convert to list - "path" may be an instance of _NamespacePath (which doesn't support indexing)
|
|
41
|
-
if path and not isinstance(path, list):
|
|
42
|
-
path = list(getattr(path, "_path", []))
|
|
43
|
-
|
|
44
|
-
if not path:
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
name = fullname.split(".")[-1]
|
|
48
|
-
|
|
49
|
-
file_path = os.path.join(path[0], name + ".py")
|
|
50
|
-
enc = file_path + ".enc"
|
|
51
|
-
|
|
52
|
-
if not os.path.isfile(enc):
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
if os.path.isfile(file_path):
|
|
56
|
-
# file already exists in decrypted state, let next import handler handle it
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
return spec_from_file_location(
|
|
60
|
-
fullname, enc, loader=DecryptingLoader(enc, self.decryption_handler)
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class DecryptingLoader(SourceLoader):
|
|
65
|
-
decryption_handler: DecryptionHandler
|
|
66
|
-
|
|
67
|
-
def __init__(self, encrypted_file, decryption_handler: DecryptionHandler):
|
|
68
|
-
self.encrypted_file = encrypted_file
|
|
69
|
-
self.decryption_handler = decryption_handler
|
|
70
|
-
|
|
71
|
-
def get_filename(self, fullname):
|
|
72
|
-
return self.encrypted_file
|
|
73
|
-
|
|
74
|
-
def get_data(self, filename):
|
|
75
|
-
with open(filename, "rb") as f:
|
|
76
|
-
data = f.read()
|
|
77
|
-
data = self.decryption_handler.decrypt(data)
|
|
78
|
-
return data
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def init_source_decryption(decryption_handler: DecryptionHandler):
|
|
82
|
-
# insert custom file loader into meta_path
|
|
83
|
-
sys.meta_path.insert(0, EncryptedFileFinder(decryption_handler))
|
|
84
|
-
# patch traceback/inspect to gracefully handle encrypted source files
|
|
85
|
-
patch_traceback_lines()
|
|
86
|
-
patch_inspect_findsource()
|
|
87
|
-
patch_tokenize_open(decryption_handler)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def patch_traceback_lines():
|
|
91
|
-
"""Patch traceback to gracefully handle encrypted source files. This is required to avoid
|
|
92
|
-
UnicodeDecodeError errors when traceback attempts to access the binary file content."""
|
|
93
|
-
|
|
94
|
-
# make sure we apply the patch only once
|
|
95
|
-
if getattr(traceback.FrameSummary, "_ls_patch_applied", None):
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def line(self):
|
|
100
|
-
try:
|
|
101
|
-
return line_orig.fget(self)
|
|
102
|
-
except Exception:
|
|
103
|
-
self._line = ""
|
|
104
|
-
return self._line
|
|
105
|
-
|
|
106
|
-
line_orig = traceback.FrameSummary.line
|
|
107
|
-
traceback.FrameSummary.line = line
|
|
108
|
-
traceback.FrameSummary._ls_patch_applied = True
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def patch_inspect_findsource():
|
|
112
|
-
"""Patch inspect to gracefully handle encrypted source files. This is required to avoid
|
|
113
|
-
"SyntaxError: invalid or missing encoding declaration" when attempting to access the binary file content.
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
# TODO: this patch is likely no longer required, should be handled by patch_tokenize_open(..) below
|
|
117
|
-
|
|
118
|
-
# make sure we apply the patch only once
|
|
119
|
-
if getattr(inspect, "_ls_patch_applied", None):
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
def findsource(*args, **kwargs):
|
|
123
|
-
try:
|
|
124
|
-
return findsource_orig(*args, **kwargs)
|
|
125
|
-
except Exception:
|
|
126
|
-
return [], 0
|
|
127
|
-
|
|
128
|
-
findsource_orig = inspect.findsource
|
|
129
|
-
inspect.findsource = findsource
|
|
130
|
-
inspect._ls_patch_applied = True
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def patch_tokenize_open(decryption_handler: DecryptionHandler):
|
|
134
|
-
"""
|
|
135
|
-
Patch to gracefully handle encrypted source files. This is required to properly retrieve the source
|
|
136
|
-
code of encrypted Python files when requested. In particular, it enables the use of code inspection utilities
|
|
137
|
-
in the `inspect` module, for example inspect.getsource(..) which returns the source code of a given module.
|
|
138
|
-
|
|
139
|
-
Note: this patch still leaves the in-memory file decryption of the `DecryptingLoader(..)` above intact,
|
|
140
|
-
and hence has no negative impact on the runtime performance of decrypting the source files on startup.
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
@patch(tokenize.open)
|
|
144
|
-
def tokenize_open(fn, filename, *args, **kwargs):
|
|
145
|
-
try:
|
|
146
|
-
return fn(filename, *args, **kwargs)
|
|
147
|
-
except Exception as e:
|
|
148
|
-
# Special handling for loading encrypted python files. In case we run inspect.getsource(..) on
|
|
149
|
-
# an encrypted module, we'd receive the following error message:
|
|
150
|
-
# SyntaxError: invalid or missing encoding declaration for '/.../my_module.py.enc'
|
|
151
|
-
if "missing encoding declaration" in str(e) and filename.endswith(".py.enc"):
|
|
152
|
-
# below we load the content of the encrypted file, decrypt it via `decryption_handler`,
|
|
153
|
-
# then store it to a temporary file, and call tokenize.open(..) on the now plaintext file
|
|
154
|
-
content = load_file(filename, mode="rb")
|
|
155
|
-
content_decrypted = to_str(decryption_handler.decrypt(content))
|
|
156
|
-
with tempfile.NamedTemporaryFile("w+t") as fp:
|
|
157
|
-
fp.write(content_decrypted)
|
|
158
|
-
fp.seek(0)
|
|
159
|
-
return fn(fp.name, *args, **kwargs)
|
|
160
|
-
raise
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Any, Protocol
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Payload(Protocol):
|
|
8
|
-
def as_dict(self) -> dict[str, Any]: ...
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Metric(ABC):
|
|
12
|
-
"""
|
|
13
|
-
Base class for all metrics (e.g., Counter, Gauge).
|
|
14
|
-
Each subclass must implement the `collect()` method.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
_namespace: str
|
|
18
|
-
_name: str
|
|
19
|
-
_schema_version: int
|
|
20
|
-
|
|
21
|
-
def __init__(self, namespace: str, name: str, schema_version: int = 1):
|
|
22
|
-
if not namespace or namespace.strip() == "":
|
|
23
|
-
raise ValueError("Namespace must be non-empty string.")
|
|
24
|
-
self._namespace = namespace
|
|
25
|
-
|
|
26
|
-
if not name or name.strip() == "":
|
|
27
|
-
raise ValueError("Metric name must be non-empty string.")
|
|
28
|
-
self._name = name
|
|
29
|
-
|
|
30
|
-
if schema_version is None:
|
|
31
|
-
raise ValueError("An explicit schema_version is required for Counter metrics")
|
|
32
|
-
|
|
33
|
-
if not isinstance(schema_version, int):
|
|
34
|
-
raise TypeError("Schema version must be an integer.")
|
|
35
|
-
|
|
36
|
-
if schema_version <= 0:
|
|
37
|
-
raise ValueError("Schema version must be greater than zero.")
|
|
38
|
-
|
|
39
|
-
self._schema_version = schema_version
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def namespace(self) -> str:
|
|
43
|
-
return self._namespace
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def name(self) -> str:
|
|
47
|
-
return self._name
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def schema_version(self) -> int:
|
|
51
|
-
return self._schema_version
|
|
52
|
-
|
|
53
|
-
@abstractmethod
|
|
54
|
-
def collect(self) -> list[Payload]:
|
|
55
|
-
"""
|
|
56
|
-
Collects and returns metric data. Subclasses must implement this to return collected metric data.
|
|
57
|
-
"""
|
|
58
|
-
pass
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from collections import defaultdict
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from localstack_cli import config
|
|
7
|
-
|
|
8
|
-
from .api import Metric
|
|
9
|
-
from .registry import MetricRegistry
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class CounterPayload:
|
|
14
|
-
"""A data object storing the value of a Counter metric."""
|
|
15
|
-
|
|
16
|
-
namespace: str
|
|
17
|
-
name: str
|
|
18
|
-
value: int
|
|
19
|
-
type: str
|
|
20
|
-
schema_version: int
|
|
21
|
-
|
|
22
|
-
def as_dict(self) -> dict[str, Any]:
|
|
23
|
-
return {
|
|
24
|
-
"namespace": self.namespace,
|
|
25
|
-
"name": self.name,
|
|
26
|
-
"value": self.value,
|
|
27
|
-
"type": self.type,
|
|
28
|
-
"schema_version": self.schema_version,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class LabeledCounterPayload:
|
|
34
|
-
"""A data object storing the value of a LabeledCounter metric."""
|
|
35
|
-
|
|
36
|
-
namespace: str
|
|
37
|
-
name: str
|
|
38
|
-
value: int
|
|
39
|
-
type: str
|
|
40
|
-
schema_version: int
|
|
41
|
-
labels: dict[str, str | float]
|
|
42
|
-
|
|
43
|
-
def as_dict(self) -> dict[str, Any]:
|
|
44
|
-
payload_dict = {
|
|
45
|
-
"namespace": self.namespace,
|
|
46
|
-
"name": self.name,
|
|
47
|
-
"value": self.value,
|
|
48
|
-
"type": self.type,
|
|
49
|
-
"schema_version": self.schema_version,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for i, (label_name, label_value) in enumerate(self.labels.items(), 1):
|
|
53
|
-
payload_dict[f"label_{i}"] = label_name
|
|
54
|
-
payload_dict[f"label_{i}_value"] = label_value
|
|
55
|
-
|
|
56
|
-
return payload_dict
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class ThreadSafeCounter:
|
|
60
|
-
"""
|
|
61
|
-
A thread-safe counter for any kind of tracking.
|
|
62
|
-
This class should not be instantiated directly, use Counter or LabeledCounter instead.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
_mutex: threading.Lock
|
|
66
|
-
_count: int
|
|
67
|
-
|
|
68
|
-
def __init__(self):
|
|
69
|
-
super().__init__()
|
|
70
|
-
self._mutex = threading.Lock()
|
|
71
|
-
self._count = 0
|
|
72
|
-
|
|
73
|
-
@property
|
|
74
|
-
def count(self) -> int:
|
|
75
|
-
return self._count
|
|
76
|
-
|
|
77
|
-
def increment(self, value: int = 1) -> None:
|
|
78
|
-
"""Increments the counter unless events are disabled."""
|
|
79
|
-
if config.DISABLE_EVENTS:
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
if value <= 0:
|
|
83
|
-
raise ValueError("Increment value must be positive.")
|
|
84
|
-
|
|
85
|
-
with self._mutex:
|
|
86
|
-
self._count += value
|
|
87
|
-
|
|
88
|
-
def reset(self) -> None:
|
|
89
|
-
"""Resets the counter to zero unless events are disabled."""
|
|
90
|
-
if config.DISABLE_EVENTS:
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
with self._mutex:
|
|
94
|
-
self._count = 0
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class Counter(Metric, ThreadSafeCounter):
|
|
98
|
-
"""
|
|
99
|
-
A thread-safe, unlabeled counter for tracking the total number of occurrences of a specific event.
|
|
100
|
-
This class is intended for metrics that do not require differentiation across dimensions.
|
|
101
|
-
For use cases where metrics need to be grouped or segmented by labels, use `LabeledCounter` instead.
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
_type: str
|
|
105
|
-
|
|
106
|
-
def __init__(self, namespace: str, name: str, schema_version: int = 1):
|
|
107
|
-
Metric.__init__(self, namespace=namespace, name=name, schema_version=schema_version)
|
|
108
|
-
ThreadSafeCounter.__init__(self)
|
|
109
|
-
|
|
110
|
-
self._type = "counter"
|
|
111
|
-
MetricRegistry().register(self)
|
|
112
|
-
|
|
113
|
-
def collect(self) -> list[CounterPayload]:
|
|
114
|
-
"""Collects the metric unless events are disabled."""
|
|
115
|
-
if config.DISABLE_EVENTS:
|
|
116
|
-
return []
|
|
117
|
-
|
|
118
|
-
if self._count == 0:
|
|
119
|
-
# Return an empty list if the count is 0, as there are no metrics to send to the analytics backend.
|
|
120
|
-
return []
|
|
121
|
-
|
|
122
|
-
return [
|
|
123
|
-
CounterPayload(
|
|
124
|
-
namespace=self._namespace,
|
|
125
|
-
name=self.name,
|
|
126
|
-
value=self._count,
|
|
127
|
-
type=self._type,
|
|
128
|
-
schema_version=self._schema_version,
|
|
129
|
-
)
|
|
130
|
-
]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class LabeledCounter(Metric):
|
|
134
|
-
"""
|
|
135
|
-
A thread-safe counter for tracking occurrences of an event across multiple combinations of label values.
|
|
136
|
-
It enables fine-grained metric collection and analysis, with each unique label set stored and counted independently.
|
|
137
|
-
Use this class when you need dimensional insights into event occurrences.
|
|
138
|
-
For simpler, unlabeled use cases, see the `Counter` class.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
_type: str
|
|
142
|
-
_labels: list[str]
|
|
143
|
-
_label_values: tuple[str | float | None, ...]
|
|
144
|
-
_counters_by_label_values: defaultdict[tuple[str | float | None, ...], ThreadSafeCounter]
|
|
145
|
-
|
|
146
|
-
def __init__(self, namespace: str, name: str, labels: list[str], schema_version: int = 1):
|
|
147
|
-
super().__init__(namespace=namespace, name=name, schema_version=schema_version)
|
|
148
|
-
|
|
149
|
-
if not labels:
|
|
150
|
-
raise ValueError("At least one label is required; the labels list cannot be empty.")
|
|
151
|
-
|
|
152
|
-
if any(not label for label in labels):
|
|
153
|
-
raise ValueError("Labels must be non-empty strings.")
|
|
154
|
-
|
|
155
|
-
if len(labels) > 6:
|
|
156
|
-
raise ValueError("Too many labels: counters allow a maximum of 6.")
|
|
157
|
-
|
|
158
|
-
self._type = "counter"
|
|
159
|
-
self._labels = labels
|
|
160
|
-
self._counters_by_label_values = defaultdict(ThreadSafeCounter)
|
|
161
|
-
MetricRegistry().register(self)
|
|
162
|
-
|
|
163
|
-
def labels(self, **kwargs: str | float | None) -> ThreadSafeCounter:
|
|
164
|
-
"""
|
|
165
|
-
Create a scoped counter instance with specific label values.
|
|
166
|
-
|
|
167
|
-
This method assigns values to the predefined labels of a labeled counter and returns
|
|
168
|
-
a ThreadSafeCounter object that allows tracking metrics for that specific
|
|
169
|
-
combination of label values.
|
|
170
|
-
|
|
171
|
-
:raises ValueError:
|
|
172
|
-
- If the set of keys provided labels does not match the expected set of labels.
|
|
173
|
-
"""
|
|
174
|
-
if set(self._labels) != set(kwargs.keys()):
|
|
175
|
-
raise ValueError(f"Expected labels {self._labels}, got {list(kwargs.keys())}")
|
|
176
|
-
|
|
177
|
-
_label_values = tuple(kwargs[label] for label in self._labels)
|
|
178
|
-
|
|
179
|
-
return self._counters_by_label_values[_label_values]
|
|
180
|
-
|
|
181
|
-
def collect(self) -> list[LabeledCounterPayload]:
|
|
182
|
-
if config.DISABLE_EVENTS:
|
|
183
|
-
return []
|
|
184
|
-
|
|
185
|
-
payload = []
|
|
186
|
-
num_labels = len(self._labels)
|
|
187
|
-
|
|
188
|
-
for label_values, counter in self._counters_by_label_values.items():
|
|
189
|
-
if counter.count == 0:
|
|
190
|
-
continue # Skip items with a count of 0, as they should not be sent to the analytics backend.
|
|
191
|
-
|
|
192
|
-
if len(label_values) != num_labels:
|
|
193
|
-
raise ValueError(
|
|
194
|
-
f"Label count mismatch: expected {num_labels} labels {self._labels}, "
|
|
195
|
-
f"but got {len(label_values)} values {label_values}."
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
# Create labels dictionary
|
|
199
|
-
labels_dict = dict(zip(self._labels, label_values, strict=False))
|
|
200
|
-
|
|
201
|
-
payload.append(
|
|
202
|
-
LabeledCounterPayload(
|
|
203
|
-
namespace=self._namespace,
|
|
204
|
-
name=self.name,
|
|
205
|
-
value=counter.count,
|
|
206
|
-
type=self._type,
|
|
207
|
-
schema_version=self._schema_version,
|
|
208
|
-
labels=labels_dict,
|
|
209
|
-
)
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
return payload
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
|
|
3
|
-
from localstack_cli import config
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
from localstack_cli.runtime import hooks
|
|
7
|
-
except ImportError:
|
|
8
|
-
hooks = None
|
|
9
|
-
|
|
10
|
-
from localstack_cli.utils.analytics import get_session_id
|
|
11
|
-
from localstack_cli.utils.analytics.events import Event, EventMetadata
|
|
12
|
-
from localstack_cli.utils.analytics.publisher import AnalyticsClientPublisher
|
|
13
|
-
|
|
14
|
-
from .registry import MetricRegistry
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def publish_metrics() -> None:
|
|
18
|
-
"""
|
|
19
|
-
Collects all the registered metrics and immediately sends them to the analytics service.
|
|
20
|
-
Skips execution if event tracking is disabled (`config.DISABLE_EVENTS`).
|
|
21
|
-
|
|
22
|
-
This function is automatically triggered on infrastructure shutdown.
|
|
23
|
-
"""
|
|
24
|
-
if config.DISABLE_EVENTS:
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
collected_metrics = MetricRegistry().collect()
|
|
28
|
-
if not collected_metrics.payload: # Skip publishing if no metrics remain after filtering
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
metadata = EventMetadata(
|
|
32
|
-
session_id=get_session_id(),
|
|
33
|
-
client_time=str(datetime.now()),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
if collected_metrics:
|
|
37
|
-
publisher = AnalyticsClientPublisher()
|
|
38
|
-
publisher.publish(
|
|
39
|
-
[Event(name="ls_metrics", metadata=metadata, payload=collected_metrics.as_dict())]
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Register the hook if runtime is available
|
|
44
|
-
if hooks is not None:
|
|
45
|
-
publish_metrics = hooks.on_infra_shutdown()(publish_metrics)
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import threading
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from .api import Metric, Payload
|
|
9
|
-
|
|
10
|
-
LOG = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class MetricPayload:
|
|
15
|
-
"""
|
|
16
|
-
A data object storing the value of all metrics collected during the execution of the application.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
_payload: list[Payload]
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def payload(self) -> list[Payload]:
|
|
23
|
-
return self._payload
|
|
24
|
-
|
|
25
|
-
def __init__(self, payload: list[Payload]):
|
|
26
|
-
self._payload = payload
|
|
27
|
-
|
|
28
|
-
def as_dict(self) -> dict[str, list[dict[str, Any]]]:
|
|
29
|
-
return {"metrics": [payload.as_dict() for payload in self._payload]}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class MetricRegistryKey:
|
|
34
|
-
"""A unique identifier for a metric, composed of namespace and name."""
|
|
35
|
-
|
|
36
|
-
namespace: str
|
|
37
|
-
name: str
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class MetricRegistry:
|
|
41
|
-
"""
|
|
42
|
-
A Singleton class responsible for managing all registered metrics.
|
|
43
|
-
Provides methods for retrieving and collecting metrics.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
_instance: MetricRegistry = None
|
|
47
|
-
_mutex: threading.Lock = threading.Lock()
|
|
48
|
-
|
|
49
|
-
def __new__(cls):
|
|
50
|
-
# avoid locking if the instance already exist
|
|
51
|
-
if cls._instance is None:
|
|
52
|
-
with cls._mutex:
|
|
53
|
-
# Prevents race conditions when multiple threads enter the first check simultaneously
|
|
54
|
-
if cls._instance is None:
|
|
55
|
-
cls._instance = super().__new__(cls)
|
|
56
|
-
return cls._instance
|
|
57
|
-
|
|
58
|
-
def __init__(self):
|
|
59
|
-
if not hasattr(self, "_registry"):
|
|
60
|
-
self._registry = {}
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def registry(self) -> dict[MetricRegistryKey, Metric]:
|
|
64
|
-
return self._registry
|
|
65
|
-
|
|
66
|
-
def register(self, metric: Metric) -> None:
|
|
67
|
-
"""
|
|
68
|
-
Registers a metric instance.
|
|
69
|
-
|
|
70
|
-
Raises a TypeError if the object is not a Metric,
|
|
71
|
-
or a ValueError if a metric with the same namespace and name is already registered
|
|
72
|
-
"""
|
|
73
|
-
if not isinstance(metric, Metric):
|
|
74
|
-
raise TypeError("Only subclasses of `Metric` can be registered.")
|
|
75
|
-
|
|
76
|
-
if not metric.namespace:
|
|
77
|
-
raise ValueError("Metric 'namespace' must be defined and non-empty.")
|
|
78
|
-
|
|
79
|
-
registry_unique_key = MetricRegistryKey(namespace=metric.namespace, name=metric.name)
|
|
80
|
-
if registry_unique_key in self._registry:
|
|
81
|
-
raise ValueError(
|
|
82
|
-
f"A metric named '{metric.name}' already exists in the '{metric.namespace}' namespace"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
self._registry[registry_unique_key] = metric
|
|
86
|
-
|
|
87
|
-
def collect(self) -> MetricPayload:
|
|
88
|
-
"""
|
|
89
|
-
Collects all registered metrics.
|
|
90
|
-
"""
|
|
91
|
-
payload = [
|
|
92
|
-
metric
|
|
93
|
-
for metric_instance in self._registry.values()
|
|
94
|
-
for metric in metric_instance.collect()
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
return MetricPayload(payload=payload)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
try:
|
|
2
|
-
from localstack_cli.runtime import hooks
|
|
3
|
-
except ImportError:
|
|
4
|
-
# Runtime not available - this module requires runtime dependencies
|
|
5
|
-
pass
|
|
6
|
-
else:
|
|
7
|
-
@hooks.on_runtime_ready()
|
|
8
|
-
def publish_provider_assignment():
|
|
9
|
-
"""
|
|
10
|
-
Publishes the service provider assignment to the analytics service.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from localstack_cli.config import SERVICE_PROVIDER_CONFIG
|
|
14
|
-
from localstack_cli.services.plugins import SERVICE_PLUGINS
|
|
15
|
-
from localstack_cli.utils.analytics import log
|
|
16
|
-
|
|
17
|
-
provider_assignment = {
|
|
18
|
-
service: f"localstack.aws.provider/{service}:{SERVICE_PROVIDER_CONFIG[service]}"
|
|
19
|
-
for service in SERVICE_PLUGINS.list_available()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
log.event("ls_service_provider_assignment", provider_assignment)
|
|
File without changes
|
|
File without changes
|