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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localstack
3
- Version: 4.13.2.dev3
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.10
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=SFLLRw8MUMPC4Sz9qchuuWw3ogigwBorq4pH1XS9imw,719
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=1hhW6wQX9vASWPgB2otqzsLcu5o8hFbm1KnzG27Z1q4,51780
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=fC2-tE2S9Lp91IvsteJZ0KaDNhKiz_oPS6bXDTsDJhM,51612
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=hEen4GT4WWgjuJ0eEk9KRbezQ8WZGsfJ8XoKjtlNOMc,7556
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.dev3.dist-info/METADATA,sha256=MWeqCkXHPdsmbctx59WUfU8M7FjrU0j5ACP8-hj-Fiw,2706
109
- localstack-4.13.2.dev3.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
110
- localstack-4.13.2.dev3.dist-info/entry_points.txt,sha256=-XGpYRc-r6x3LIbKbTAlmNSYx5uaQ2LZOoI00lSZ3wI,201
111
- localstack-4.13.2.dev3.dist-info/top_level.txt,sha256=02blzxjv7TnyriE5gkWIM_UqKTSBiCFPgRG5ZBO5R0Y,15
112
- localstack-4.13.2.dev3.dist-info/RECORD,,
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, to_str
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 first activates the license using credentials from the environment, and then
830
- enables code decryption if necessary. Once completed, it sets ``self.license`` to the license document
831
- that was activated. This method will also set the env var ``PRO_ACTIVATED=1`` after successful license
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,6 @@
1
+ """Runtime utilities for the LocalStack CLI."""
2
+
3
+ from localstack_cli.runtime import hooks # noqa: F401
4
+
5
+ # get_current_runtime is not available in the standalone CLI - only in the full LocalStack runtime
6
+ get_current_runtime = None
@@ -0,0 +1,7 @@
1
+ class LocalstackExit(Exception):
2
+ """Raised to gracefully exit LocalStack."""
3
+
4
+ def __init__(self, reason: str = None, code: int = 1):
5
+ self.reason = reason
6
+ self.code = code
7
+ super().__init__(reason)
@@ -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."""
@@ -0,0 +1,4 @@
1
+ """Testing configuration constants."""
2
+
3
+ # Secondary test account ID for multi-account testing scenarios
4
+ SECONDARY_TEST_AWS_ACCOUNT_ID = "886468871268"
@@ -5,11 +5,7 @@ import platform
5
5
 
6
6
  from localstack_cli import config
7
7
  from localstack_cli.constants import VERSION
8
- try:
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
- # Runtime-only hooks - only register when running inside LocalStack runtime
241
- if hooks is not None:
242
- @hooks.prepare_host()
243
- def prepare_host_machine_id():
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
- # mount tha machine file from the host's CLI cache directory into the appropriate location in the
252
- # container
253
- machine_file = os.path.join(config.dirs.cache, "machine.json")
254
- if os.path.isfile(machine_file):
255
- target = os.path.join(config.dirs.for_container().cache, "machine.json")
256
- container.config.volumes.add(BindMount(machine_file, target, read_only=True))
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
- try:
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
- if hooks:
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
- if hooks:
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.dev3'
32
- __version_tuple__ = version_tuple = (4, 13, 2, 'dev3')
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,6 +0,0 @@
1
- [console_scripts]
2
- localstack = localstack_cli.cli.main:main
3
-
4
- [localstack_cli.plugins.cli]
5
- core = localstack_cli.cli.core_plugin:CoreCliPlugin
6
- pro = localstack_cli.pro.core.cli.localstack:ProCliPlugins
@@ -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,6 +0,0 @@
1
- """LocalStack metrics instrumentation framework"""
2
-
3
- from .counter import Counter, LabeledCounter
4
- from .registry import MetricRegistry, MetricRegistryKey
5
-
6
- __all__ = ["Counter", "LabeledCounter", "MetricRegistry", "MetricRegistryKey"]
@@ -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)