playground-ls-cli 4.14.1.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,294 @@
1
+ import io
2
+ import logging
3
+ import os
4
+ import re
5
+ import threading
6
+
7
+ from asn1crypto import algos, cms, core
8
+ from asn1crypto import x509 as asn1_x509
9
+ from cryptography.hazmat.backends import default_backend
10
+ from cryptography.hazmat.primitives import hashes
11
+ from cryptography.hazmat.primitives import padding as sym_padding
12
+ from cryptography.hazmat.primitives.asymmetric import padding
13
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
14
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
15
+
16
+ from .files import TMP_FILES, file_exists_not_empty, load_file, new_tmp_file, save_file
17
+ from .strings import short_uid, to_bytes, to_str
18
+ from .sync import synchronized
19
+ from .urls import localstack_host
20
+
21
+ LOG = logging.getLogger(__name__)
22
+
23
+ # block size for symmetric encrypt/decrypt operations
24
+ BLOCK_SIZE = 16
25
+
26
+ # lock for creating certificate files
27
+ SSL_CERT_LOCK = threading.RLock()
28
+
29
+ # markers that indicate the start/end of sections in PEM cert files
30
+ PEM_CERT_START = "-----BEGIN CERTIFICATE-----"
31
+ PEM_CERT_END = "-----END CERTIFICATE-----"
32
+ PEM_KEY_START_REGEX = r"-----BEGIN(.*)PRIVATE KEY-----"
33
+ PEM_KEY_END_REGEX = r"-----END(.*)PRIVATE KEY-----"
34
+
35
+ OID_AES256_CBC = "2.16.840.1.101.3.4.1.42"
36
+ OID_MGF1 = "1.2.840.113549.1.1.8"
37
+ OID_RSAES_OAEP = "1.2.840.113549.1.1.7"
38
+ OID_SHA256 = "2.16.840.1.101.3.4.2.1"
39
+
40
+
41
+ @synchronized(lock=SSL_CERT_LOCK)
42
+ def generate_ssl_cert(
43
+ target_file=None,
44
+ overwrite=False,
45
+ random=False,
46
+ return_content=False,
47
+ serial_number=None,
48
+ ):
49
+ # Note: Do NOT import "OpenSSL" at the root scope
50
+ # (Our test Lambdas are importing this file but don't have the module installed)
51
+ from OpenSSL import crypto
52
+
53
+ def all_exist(*files):
54
+ return all(os.path.exists(f) for f in files)
55
+
56
+ def store_cert_key_files(base_filename):
57
+ key_file_name = f"{base_filename}.key"
58
+ cert_file_name = f"{base_filename}.crt"
59
+ # TODO: Cleaner code to load the cert dynamically
60
+ # extract key and cert from target_file and store into separate files
61
+ content = load_file(target_file)
62
+ key_start = re.search(PEM_KEY_START_REGEX, content)
63
+ key_start = key_start.group(0)
64
+ key_end = re.search(PEM_KEY_END_REGEX, content)
65
+ key_end = key_end.group(0)
66
+ key_content = content[content.index(key_start) : content.index(key_end) + len(key_end)]
67
+ cert_content = content[
68
+ content.index(PEM_CERT_START) : content.rindex(PEM_CERT_END) + len(PEM_CERT_END)
69
+ ]
70
+ save_file(key_file_name, key_content)
71
+ save_file(cert_file_name, cert_content)
72
+ return cert_file_name, key_file_name
73
+
74
+ if target_file and not overwrite and file_exists_not_empty(target_file):
75
+ try:
76
+ cert_file_name, key_file_name = store_cert_key_files(target_file)
77
+ except Exception as e:
78
+ # fall back to temporary files if we cannot store/overwrite the files above
79
+ LOG.info(
80
+ "Error storing key/cert SSL files (falling back to random tmp file names): %s", e
81
+ )
82
+ target_file_tmp = new_tmp_file()
83
+ cert_file_name, key_file_name = store_cert_key_files(target_file_tmp)
84
+ if all_exist(cert_file_name, key_file_name):
85
+ return target_file, cert_file_name, key_file_name
86
+ if random and target_file:
87
+ if "." in target_file:
88
+ target_file = target_file.replace(".", f".{short_uid()}.", 1)
89
+ else:
90
+ target_file = f"{target_file}.{short_uid()}"
91
+
92
+ # create a key pair
93
+ k = crypto.PKey()
94
+ k.generate_key(crypto.TYPE_RSA, 2048)
95
+
96
+ host_definition = localstack_host()
97
+
98
+ # create a self-signed cert
99
+ cert = crypto.X509()
100
+ subj = cert.get_subject()
101
+ subj.C = "AU"
102
+ subj.ST = "Some-State"
103
+ subj.L = "Some-Locality"
104
+ subj.O = "LocalStack Org" # noqa
105
+ subj.OU = "Testing"
106
+ subj.CN = "localhost"
107
+ # Note: new requirements for recent OSX versions: https://support.apple.com/en-us/HT210176
108
+ # More details: https://www.iol.unh.edu/blog/2019/10/10/macos-catalina-and-chrome-trust
109
+ serial_number = serial_number or 1001
110
+ cert.set_version(2)
111
+ cert.set_serial_number(serial_number)
112
+ cert.gmtime_adj_notBefore(0)
113
+ cert.gmtime_adj_notAfter(2 * 365 * 24 * 60 * 60)
114
+ cert.set_issuer(cert.get_subject())
115
+ cert.set_pubkey(k)
116
+ alt_names = (
117
+ f"DNS:localhost,DNS:test.localhost.atlassian.io,DNS:localhost.localstack.cloud,DNS:{host_definition.host}IP:127.0.0.1"
118
+ ).encode()
119
+ cert.add_extensions(
120
+ [
121
+ crypto.X509Extension(b"subjectAltName", False, alt_names),
122
+ crypto.X509Extension(b"basicConstraints", True, b"CA:false"),
123
+ crypto.X509Extension(
124
+ b"keyUsage", True, b"nonRepudiation,digitalSignature,keyEncipherment"
125
+ ),
126
+ crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"),
127
+ ]
128
+ )
129
+ cert.sign(k, "SHA256")
130
+
131
+ cert_file = io.StringIO()
132
+ key_file = io.StringIO()
133
+ cert_file.write(to_str(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)))
134
+ key_file.write(to_str(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)))
135
+ cert_file_content = cert_file.getvalue().strip()
136
+ key_file_content = key_file.getvalue().strip()
137
+ file_content = f"{key_file_content}\n{cert_file_content}"
138
+ if target_file:
139
+ key_file_name = f"{target_file}.key"
140
+ cert_file_name = f"{target_file}.crt"
141
+ # check existence to avoid permission denied issues:
142
+ # https://github.com/localstack/localstack/issues/1607
143
+ if not all_exist(target_file, key_file_name, cert_file_name):
144
+ for i in range(2):
145
+ try:
146
+ save_file(target_file, file_content)
147
+ save_file(key_file_name, key_file_content)
148
+ save_file(cert_file_name, cert_file_content)
149
+ break
150
+ except Exception as e:
151
+ if i > 0:
152
+ raise
153
+ LOG.info(
154
+ "Unable to store certificate file under %s, using tmp file instead: %s",
155
+ target_file,
156
+ e,
157
+ )
158
+ # Fix for https://github.com/localstack/localstack/issues/1743
159
+ target_file = f"{new_tmp_file()}.pem"
160
+ key_file_name = f"{target_file}.key"
161
+ cert_file_name = f"{target_file}.crt"
162
+ TMP_FILES.append(target_file)
163
+ TMP_FILES.append(key_file_name)
164
+ TMP_FILES.append(cert_file_name)
165
+ if not return_content:
166
+ return target_file, cert_file_name, key_file_name
167
+ return file_content
168
+
169
+
170
+ def pad(s: bytes) -> bytes:
171
+ return s + to_bytes((BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE))
172
+
173
+
174
+ def unpad(s: bytes) -> bytes:
175
+ return s[0 : -s[-1]]
176
+
177
+
178
+ def encrypt(key: bytes, message: bytes, iv: bytes = None, aad: bytes = None) -> tuple[bytes, bytes]:
179
+ iv = iv or b"0" * BLOCK_SIZE
180
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
181
+ encryptor = cipher.encryptor()
182
+ encryptor.authenticate_additional_data(aad)
183
+ encrypted = encryptor.update(pad(message)) + encryptor.finalize()
184
+ return encrypted, encryptor.tag
185
+
186
+
187
+ def decrypt(
188
+ key: bytes, encrypted: bytes, iv: bytes = None, tag: bytes = None, aad: bytes = None
189
+ ) -> bytes:
190
+ iv = iv or b"0" * BLOCK_SIZE
191
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
192
+ decryptor = cipher.decryptor()
193
+ decryptor.authenticate_additional_data(aad)
194
+ decrypted = decryptor.update(encrypted) + decryptor.finalize()
195
+ decrypted = unpad(decrypted)
196
+ return decrypted
197
+
198
+
199
+ def pkcs7_envelope_encrypt(plaintext: bytes, recipient_pubkey: RSAPublicKey) -> bytes:
200
+ """
201
+ Create a PKCS7 wrapper of some plaintext decryptable by recipient_pubkey. Uses RSA-OAEP with SHA-256
202
+ to encrypt the AES-256-CBC content key. Hazmat's PKCS7EnvelopeBuilder doesn't support RSA-OAEP with SHA-256,
203
+ so we need to build the pieces manually and then put them together in an envelope with asn1crypto.
204
+ """
205
+
206
+ # Encrypt the plaintext with an AES session key, then encrypt the session key to the recipient_pubkey
207
+ session_key = os.urandom(32)
208
+ iv = os.urandom(16)
209
+ encrypted_session_key = recipient_pubkey.encrypt(
210
+ session_key,
211
+ padding.OAEP(
212
+ mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None
213
+ ),
214
+ )
215
+ cipher = Cipher(algorithms.AES(session_key), modes.CBC(iv), backend=default_backend())
216
+ encryptor = cipher.encryptor()
217
+ padder = sym_padding.PKCS7(algorithms.AES.block_size).padder()
218
+ padded_plaintext = padder.update(plaintext) + padder.finalize()
219
+ encrypted_content = encryptor.update(padded_plaintext) + encryptor.finalize()
220
+
221
+ # Now put together the envelope.
222
+ # Add the recipient with their copy of the session key
223
+ recipient_identifier = cms.RecipientIdentifier(
224
+ name="issuer_and_serial_number",
225
+ value=cms.IssuerAndSerialNumber(
226
+ {
227
+ "issuer": asn1_x509.Name.build({"common_name": "recipient"}),
228
+ "serial_number": 1,
229
+ }
230
+ ),
231
+ )
232
+ key_enc_algorithm = cms.KeyEncryptionAlgorithm(
233
+ {
234
+ "algorithm": OID_RSAES_OAEP,
235
+ "parameters": algos.RSAESOAEPParams(
236
+ {
237
+ "hash_algorithm": algos.DigestAlgorithm(
238
+ {
239
+ "algorithm": OID_SHA256,
240
+ }
241
+ ),
242
+ "mask_gen_algorithm": algos.MaskGenAlgorithm(
243
+ {
244
+ "algorithm": OID_MGF1,
245
+ "parameters": algos.DigestAlgorithm(
246
+ {
247
+ "algorithm": OID_SHA256,
248
+ }
249
+ ),
250
+ }
251
+ ),
252
+ }
253
+ ),
254
+ }
255
+ )
256
+ recipient_info = cms.KeyTransRecipientInfo(
257
+ {
258
+ "version": "v0",
259
+ "rid": recipient_identifier,
260
+ "key_encryption_algorithm": key_enc_algorithm,
261
+ "encrypted_key": encrypted_session_key,
262
+ }
263
+ )
264
+
265
+ # Add the encrypted content
266
+ content_enc_algorithm = cms.EncryptionAlgorithm(
267
+ {
268
+ "algorithm": OID_AES256_CBC,
269
+ "parameters": core.OctetString(iv),
270
+ }
271
+ )
272
+ encrypted_content_info = cms.EncryptedContentInfo(
273
+ {
274
+ "content_type": "data",
275
+ "content_encryption_algorithm": content_enc_algorithm,
276
+ "encrypted_content": encrypted_content,
277
+ }
278
+ )
279
+ enveloped_data = cms.EnvelopedData(
280
+ {
281
+ "version": "v0",
282
+ "recipient_infos": [recipient_info],
283
+ "encrypted_content_info": encrypted_content_info,
284
+ }
285
+ )
286
+
287
+ # Finally add a wrapper and return its bytes
288
+ content_info = cms.ContentInfo(
289
+ {
290
+ "content_type": "enveloped_data",
291
+ "content": enveloped_data,
292
+ }
293
+ )
294
+ return content_info.dump()
@@ -0,0 +1,272 @@
1
+ import functools
2
+ import logging
3
+ import platform
4
+ import random
5
+
6
+ from localstack_cli import config
7
+ from localstack_cli.constants import DEFAULT_VOLUME_DIR, DOCKER_IMAGE_NAME_PRO
8
+ from localstack_cli.utils.collections import ensure_list
9
+ from localstack_cli.utils.container_utils.container_client import (
10
+ ContainerClient,
11
+ DockerNotAvailable,
12
+ PortMappings,
13
+ VolumeInfo,
14
+ )
15
+ from localstack_cli.utils.net import IntOrPort, Port, PortNotAvailableException, PortRange
16
+ from localstack_cli.utils.objects import singleton_factory
17
+ from localstack_cli.utils.strings import to_str
18
+
19
+ LOG = logging.getLogger(__name__)
20
+
21
+ # port range instance used to reserve Docker container ports
22
+ PORT_START = 0
23
+ PORT_END = 65536
24
+ RANDOM_PORT_START = 1024
25
+ RANDOM_PORT_END = 65536
26
+
27
+
28
+ def is_docker_sdk_installed() -> bool:
29
+ try:
30
+ import docker # noqa: F401
31
+
32
+ return True
33
+ except ModuleNotFoundError:
34
+ return False
35
+
36
+
37
+ def create_docker_client() -> ContainerClient:
38
+ # never use the sdk client if it is not installed or not in docker - too risky for wrong version
39
+ if config.LEGACY_DOCKER_CLIENT or not is_docker_sdk_installed() or not config.is_in_docker:
40
+ from localstack_cli.utils.container_utils.docker_cmd_client import CmdDockerClient
41
+
42
+ LOG.debug(
43
+ "Using CmdDockerClient. LEGACY_DOCKER_CLIENT: %s, SDK installed: %s",
44
+ config.LEGACY_DOCKER_CLIENT,
45
+ is_docker_sdk_installed(),
46
+ )
47
+
48
+ return CmdDockerClient()
49
+ else:
50
+ from localstack_cli.utils.container_utils.docker_sdk_client import SdkDockerClient
51
+
52
+ LOG.debug(
53
+ "Using SdkDockerClient. LEGACY_DOCKER_CLIENT: %s, SDK installed: %s",
54
+ config.LEGACY_DOCKER_CLIENT,
55
+ is_docker_sdk_installed(),
56
+ )
57
+
58
+ return SdkDockerClient()
59
+
60
+
61
+ def get_current_container_id() -> str:
62
+ """
63
+ Returns the ID of the current container, or raises a ValueError if we're not in docker.
64
+
65
+ :return: the ID of the current container
66
+ """
67
+ if not config.is_in_docker:
68
+ raise ValueError("not in docker")
69
+
70
+ container_id = platform.node()
71
+ if not container_id:
72
+ raise OSError("no hostname returned to use as container id")
73
+
74
+ return container_id
75
+
76
+
77
+ def inspect_current_container_mounts() -> list[VolumeInfo]:
78
+ return DOCKER_CLIENT.inspect_container_volumes(get_current_container_id())
79
+
80
+
81
+ @functools.lru_cache
82
+ def get_default_volume_dir_mount() -> VolumeInfo | None:
83
+ """
84
+ Returns the volume information of LocalStack's DEFAULT_VOLUME_DIR (/var/lib/localstack), if mounted,
85
+ else it returns None. If we're not currently in docker a VauleError is raised. in a container, a ValueError is
86
+ raised.
87
+
88
+ :return: the volume info of the default volume dir or None
89
+ """
90
+ for volume in inspect_current_container_mounts():
91
+ if volume.destination.rstrip("/") == DEFAULT_VOLUME_DIR:
92
+ return volume
93
+
94
+ return None
95
+
96
+
97
+ def get_host_path_for_path_in_docker(path):
98
+ """
99
+ Returns the calculated host location for a given subpath of DEFAULT_VOLUME_DIR inside the localstack container.
100
+ The path **has** to be a subdirectory of DEFAULT_VOLUME_DIR (the dir itself *will not* work).
101
+
102
+ :param path: Path to be replaced (subpath of DEFAULT_VOLUME_DIR)
103
+ :return: Path on the host
104
+ """
105
+ if config.is_in_docker and DOCKER_CLIENT.has_docker():
106
+ volume = get_default_volume_dir_mount()
107
+
108
+ if volume:
109
+ if volume.type != "bind":
110
+ raise ValueError(
111
+ f"Mount to {DEFAULT_VOLUME_DIR} needs to be a bind mount for mounting to work"
112
+ )
113
+
114
+ if not path.startswith(f"{DEFAULT_VOLUME_DIR}/") and path != DEFAULT_VOLUME_DIR:
115
+ # We should be able to replace something here.
116
+ # if this warning is printed, the usage of this function is probably wrong.
117
+ # Please check if the target path is indeed prefixed by /var/lib/localstack
118
+ # if this happens, mounts may fail
119
+ LOG.warning(
120
+ "Error while performing automatic host path replacement for path '%s' to source '%s'",
121
+ path,
122
+ volume.source,
123
+ )
124
+ else:
125
+ relative_path = path.removeprefix(DEFAULT_VOLUME_DIR)
126
+ result = volume.source + relative_path
127
+ return result
128
+ else:
129
+ raise ValueError(f"No volume mounted to {DEFAULT_VOLUME_DIR}")
130
+
131
+ return path
132
+
133
+
134
+ def container_ports_can_be_bound(
135
+ ports: IntOrPort | list[IntOrPort],
136
+ address: str | None = None,
137
+ ) -> bool:
138
+ """Determine whether a given list of ports can be bound by Docker containers
139
+
140
+ :param ports: single port or list of ports to check
141
+ :return: True iff all ports can be bound
142
+ """
143
+ port_mappings = PortMappings(bind_host=address or "")
144
+ ports = ensure_list(ports)
145
+ for port in ports:
146
+ port = Port.wrap(port)
147
+ port_mappings.add(port.port, port.port, protocol=port.protocol)
148
+ try:
149
+ result = DOCKER_CLIENT.run_container(
150
+ _get_ports_check_docker_image(),
151
+ entrypoint="sh",
152
+ command=["-c", "echo test123"],
153
+ ports=port_mappings,
154
+ remove=True,
155
+ )
156
+ except DockerNotAvailable as e:
157
+ LOG.warning("Cannot perform port check because Docker is not available.")
158
+ raise e
159
+ except Exception as e:
160
+ if "port is already allocated" not in str(e) and "address already in use" not in str(e):
161
+ LOG.warning(
162
+ "Unexpected error when attempting to determine container port status",
163
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
164
+ )
165
+ return False
166
+ # TODO(srw): sometimes the command output from the docker container is "None", particularly when this function is
167
+ # invoked multiple times consecutively. Work out why.
168
+ if to_str(result[0] or "").strip() != "test123":
169
+ LOG.warning(
170
+ "Unexpected output when attempting to determine container port status: %s", result
171
+ )
172
+ return True
173
+
174
+
175
+ class _DockerPortRange(PortRange):
176
+ """
177
+ PortRange which checks whether the port can be bound on the host instead of inside the container.
178
+ """
179
+
180
+ def _port_can_be_bound(self, port: IntOrPort) -> bool:
181
+ return container_ports_can_be_bound(port)
182
+
183
+
184
+ reserved_docker_ports = _DockerPortRange(PORT_START, PORT_END)
185
+
186
+
187
+ def is_port_available_for_containers(port: IntOrPort) -> bool:
188
+ """Check whether the given port can be bound by containers and is not currently reserved"""
189
+ return not is_container_port_reserved(port) and container_ports_can_be_bound(port)
190
+
191
+
192
+ def reserve_container_port(port: IntOrPort, duration: int = None):
193
+ """Reserve the given container port for a short period of time"""
194
+ reserved_docker_ports.reserve_port(port, duration=duration)
195
+
196
+
197
+ def is_container_port_reserved(port: IntOrPort) -> bool:
198
+ """Return whether the given container port is currently reserved"""
199
+ port = Port.wrap(port)
200
+ return reserved_docker_ports.is_port_reserved(port)
201
+
202
+
203
+ def reserve_available_container_port(
204
+ duration: int = None,
205
+ port_start: int = None,
206
+ port_end: int = None,
207
+ protocol: str = None,
208
+ ) -> int:
209
+ """
210
+ Determine a free port within the given port range that can be bound by a Docker container, and reserve
211
+ the port for the given number of seconds
212
+
213
+ :param duration: the number of seconds to reserve the port (default: ~6 seconds)
214
+ :param port_start: the start of the port range to check (default: 1024)
215
+ :param port_end: the end of the port range to check (default: 65536)
216
+ :param protocol: the network protocol (default: tcp)
217
+ :return: a random port
218
+ :raises PortNotAvailableException: if no port is available within the given range
219
+ """
220
+
221
+ protocol = protocol or "tcp"
222
+
223
+ def _random_port():
224
+ port = None
225
+ while not port or reserved_docker_ports.is_port_reserved(port):
226
+ port_number = random.randint(
227
+ RANDOM_PORT_START if port_start is None else port_start,
228
+ RANDOM_PORT_END if port_end is None else port_end,
229
+ )
230
+ port = Port(port=port_number, protocol=protocol)
231
+ return port
232
+
233
+ retries = 10
234
+ for i in range(retries):
235
+ port = _random_port()
236
+ try:
237
+ reserve_container_port(port, duration=duration)
238
+ return port.port
239
+ except PortNotAvailableException as e:
240
+ LOG.debug("Could not bind port %s, trying the next one: %s", port, e)
241
+
242
+ raise PortNotAvailableException(
243
+ f"Unable to determine available Docker container port after {retries} retries"
244
+ )
245
+
246
+
247
+ @singleton_factory
248
+ def _get_ports_check_docker_image() -> str:
249
+ """
250
+ Determine the Docker image to use for Docker port availability checks.
251
+ Uses either PORTS_CHECK_DOCKER_IMAGE (if configured), or otherwise inspects the running container's image.
252
+ """
253
+ if config.PORTS_CHECK_DOCKER_IMAGE:
254
+ # explicit configuration takes precedence
255
+ return config.PORTS_CHECK_DOCKER_IMAGE
256
+ if not config.is_in_docker:
257
+ # local import to prevent circular imports
258
+ from localstack_cli.utils.bootstrap import get_docker_image_to_start
259
+
260
+ # Use whatever image the user is trying to run LocalStack with, since they either have
261
+ # it already, or need it by definition to start LocalStack.
262
+ return get_docker_image_to_start()
263
+ try:
264
+ # inspect the running container to determine the image
265
+ container = DOCKER_CLIENT.inspect_container(get_current_container_id())
266
+ return container["Config"]["Image"]
267
+ except Exception:
268
+ # fall back to using the default Docker image
269
+ return DOCKER_IMAGE_NAME_PRO
270
+
271
+
272
+ DOCKER_CLIENT: ContainerClient = create_docker_client()