8000 Configure cosmetic and functional variables with LOCALSTACK_HOST and … · codeperl/localstack@02c9b8f · GitHub
[go: up one dir, main page]

Skip to content

Commit 02c9b8f

Browse files
simonrwalexrashed
authored andcommitted
Configure cosmetic and functional variables with LOCALSTACK_HOST and GATEWAY_LISTEN (localstack#7893)
Switch to using `LOCALSTACK_HOST` and `GATEWAY_LISTEN` for configuring the cosmetic and functional properties of LocalStack. * `LOCALSTACK_HOST`: cosmetic - interpolated into URLs and hostnames/ports returned * `GATEWAY_LISTEN`: functional - replaces `EDGE_PORT`, `EDGE_PORT_HTTP`, `EDGE_BIND_HOST`.
1 parent 060ea65 commit 02c9b8f

File tree

4 files changed

+316
-15
lines changed

4 files changed

+316
-15
lines changed

localstack/config.py

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
import subprocess
77
import tempfile
88
import time
9-
from typing import Any, Dict, List, Mapping, Tuple
9+
from dataclasses import dataclass
10+
from typing import Any, Dict, List, Mapping, Tuple, TypeVar
1011

12+
from localstack import constants
1113
from localstack.constants import (
1214
AWS_REGION_US_EAST_1,
1315
DEFAULT_BUCKET_MARKER_LOCAL,
1416
DEFAULT_DEVELOP_PORT,
1517
DEFAULT_LAMBDA_CONTAINER_REGISTRY,
16-
DEFAULT_PORT_EDGE,
1718
DEFAULT_SERVICE_PORTS,
1819
DEFAULT_VOLUME_DIR,
1920
DOCKER_IMAGE_NAME,
@@ -28,6 +29,8 @@
2829
TRUE_STRINGS,
2930
)
3031

32+
T = TypeVar("T", str, int)
33+
3134
# keep track of start time, for performance debugging
3235
load_start_time = time.time()
3336

@@ -316,12 +319,6 @@ def in_docker():
316319
os.environ.get("DEFAULT_REGION") or os.environ.get("AWS_DEFAULT_REGION") or AWS_REGION_US_EAST_1
317320
)
318321

319-
# expose services on a specific host externally
320-
HOSTNAME_EXTERNAL = os.environ.get("HOSTNAME_EXTERNAL", "").strip() or LOCALHOST
321-
322-
# name of the host under which the LocalStack services are available
323-
LOCALSTACK_HOSTNAME = os.environ.get("LOCALSTACK_HOSTNAME", "").strip() or LOCALHOST
324-
325322
# directory for persisting data (TODO: deprecated, simply use PERSISTENCE=1)
326323
DATA_DIR = os.environ.get("DATA_DIR", "").strip()
327324

@@ -411,12 +408,120 @@ def in_docker():
411408
# whether to forward edge requests in-memory (instead of via proxy servers listening on backend ports)
412409
# TODO: this will likely become the default and may get removed in the future
413410
FORWARD_EDGE_INMEM = True
414-
# Default bind address for the edge service
415-
EDGE_BIND_HOST = os.environ.get("EDGE_BIND_HOST", "").strip() or "127.0.0.1"
416-
# port number for the edge service, the main entry point for all API invocations
417-
EDGE_PORT = int(os.environ.get("EDGE_PORT") or 0) or DEFAULT_PORT_EDGE
418-
# fallback port for non-SSL HTTP edge service (in case HTTPS edge service cannot be used)
419-
EDGE_PORT_HTTP = int(os.environ.get("EDGE_PORT_HTTP") or 0)
411+
412+
# expose services on a specific host externally
413+
# DEPRECATED: since v2.0.0 as we are moving to LOCALSTACK_HOST
414+
HOSTNAME_EXTERNAL = os.environ.get("HOSTNAME_EXTERNAL", "").strip() or LOCALHOST
415+
416+
# name of the host under which the LocalStack services are available
417+
# DEPRECATED: if the user sets this since v2.0.0 as we are moving to LOCALSTACK_HOST
418+
LOCALSTACK_HOSTNAME = os.environ.get("LOCALSTACK_HOSTNAME", "").strip() or LOCALHOST
419+
420+
421+
def populate_legacy_edge_configuration(
422+
environment: Dict[str, str]
423+
) -> Tuple[str, str, str, int, int]:
424+
if is_in_docker:
425+
default_ip = "0.0.0.0"
426+
else:
427+
default_ip = "127.0.0.1"
428+
429+
localstack_host_raw = environment.get("LOCALSTACK_HOST")
430+
gateway_listen_raw = environment.get("GATEWAY_LISTEN")
431+
432+
# new for v2
433+
# populate LOCALSTACK_HOST first since GATEWAY_LISTEN may be derived from LOCALSTACK_HOST
434+
localstack_host = localstack_host_raw
435+
if localstack_host is None:
436+
localstack_host = f"{constants.LOCALHOST_HOSTNAME}:{constants.DEFAULT_PORT_EDGE}"
437+
438+
def parse_gateway_listen(value: str) -> str:
439+
if ":" in value:
440+
ip, port_s = value.split(":", 1)
441+
if not ip.strip():
442+
ip = default_ip
443+
if not port_s.strip():
444+
port_s = "4566"
445+
port = int(port_s)
446+
return f"{ip}:{port}"
447+
else:
448+
return f"{value}:4566"
449+
450+
gateway_listen = gateway_listen_raw
451+
if gateway_listen is None:
452+
# default to existing behaviour
453+
port = int(localstack_host.split(":")[-1])
454+
gateway_listen = f"{default_ip}:{port}"
455+
else:
456+
components = gateway_listen.split(",")
457+
if len(components) > 1:
458+
LOG.warning("multiple GATEWAY_LISTEN addresses are not currently supported")
459+
460+
gateway_listen = ",".join(
461+
[parse_gateway_listen(component.strip()) for component in components]
462+
)
463+
464+
assert gateway_listen is not None
465+
assert localstack_host is not None
466+
467+
def legacy_fallback(envar_name: str, default: T) -> T:
468+
result = default
469+
result_raw = environment.get(envar_name)
470+
if result_raw is not None and gateway_listen_raw is None:
471+
result = result_raw
472+
473+
return result
474+
475+
# derive legacy variables from GATEWAY_LISTEN unless GATEWAY_LISTEN is not given and
476+
# legacy variables are
477+
edge_bind_host = legacy_fallback("EDGE_BIND_HOST", get_gateway_listen(gateway_listen)[0].host)
478+
edge_port = int(legacy_fallback("EDGE_PORT", get_gateway_listen(gateway_listen)[0].port))
479+
edge_port_http = int(
480+
legacy_fallback("EDGE_PORT_HTTP", get_gateway_listen(gateway_listen)[0].port)
481+
)
482+
483+
return localstack_host, gateway_listen, edge_bind_host, edge_port, edge_port_http
484+
485+
486+
@dataclass
487+
class HostAndPort:
488+
host: str
489+
port: int
490+
491+
@classmethod
492+
def parse(cls, input: str) -> "HostAndPort":
493+
host, port_s = input.split(":")
494+
return cls(host=host, port=int(port_s))
495+
496+
def __eq__(self, other: Any) -> bool:
497+
if not isinstance(other, self.__class__):
498+
return False
499+
500+
return self.host == other.host and self.port == other.port
501+
502+
503+
def get_gateway_listen(gateway_listen: str) -> List[HostAndPort]:
504+
result = []
505+
for bind_address in gateway_listen.split(","):
506+
result.append(HostAndPort.parse(bind_address))
507+
return result
508+
509+
510+
# How to access LocalStack
511+
(
512+
# -- Cosmetic
513+
LOCALSTACK_HOST,
514+
# -- Edge configuration
515+
# Main configuration of the listen address of the hypercorn proxy. Of the form
516+
# <ip_address>:<port>(,<ip_address>:port>)*
517+
GATEWAY_LISTEN,
518+
# -- Legacy variables
519+
EDGE_BIND_HOST,
520+
EDGE_PORT,
521+
EDGE_PORT_HTTP,
522+
) = populate_legacy_edge_configuration(os.environ)
523+
524+
420525
# optional target URL to forward all edge requests to
421526
EDGE_FORWARD_URL = os.environ.get("EDGE_FORWARD_URL", "").strip()
422527

localstack/deprecations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,36 @@ def is_affected(self) -> bool:
147147
"1.4.0",
148148
"This feature will not be supported in the future. Please remove this environment variable.",
149149
),
150+
# Since 2.0.0 - HOSTNAME_EXTERNAL will be replaced with LOCALSTACK_HOST
151+
EnvVarDeprecation(
152+
"HOSTNAME_EXTERNAL",
153+
"2.0.0",
154+
"This configuration will be migrated to LOCALSTACK_HOST",
155+
),
156+
# Since 2.0.0 - LOCALSTACK_HOST will be replaced with LOCALSTACK_HOST
157+
EnvVarDeprecation(
158+
"LOCALSTACK_HOSTNAME",
159+
"2.0.0",
160+
"This configuration will be migrated to LOCALSTACK_HOST",
161+
),
162+
# Since 2.0.0 - redefined as GATEWAY_LISTEN
163+
EnvVarDeprecation(
164+
"EDGE_BIND_HOST",
165+
"2.0.0",
166+
"This configuration will be migrated to GATEWAY_LISTEN",
167+
),
168+
# Since 2.0.0 - redefined as GATEWAY_LISTEN
169+
EnvVarDeprecation(
170+
"EDGE_PORT",
171+
"2.0.0",
172+
"This configuration will be migrated to GATEWAY_LISTEN",
173+
),
174+
# Since 2.0.0 - redefined as GATEWAY_LISTEN
175+
EnvVarDeprecation(
176+
"EDGE_PORT_HTTP",
177+
"2.0.0",
178+
"This configuration will be migrated to GATEWAY_LISTEN",
179+
),
150180
]
151181

152182

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ install_requires =
2929
cryptography
3030
dill==0.3.2
3131
dnspython>=1.16.0
32-
localstack-client>=1.37
32+
localstack-client>=2.0
3333
plux>=1.3.1
3434
psutil>=5.4.8,<6.0.0
3535
python-dotenv>=0.19.1

tests/unit/test_config.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from contextlib import contextmanager
33
from typing import Any, Dict
44

5+
import pytest
6+
57
from localstack import config
68

79

@@ -118,3 +120,167 @@ def test_custom_port_mapping_in_services_env(self):
118120
assert "foobar" in result
119121
# FOOBAR_PORT cannot be parsed
120122
assert result["foobar"] == 1235
123+
124+
125+
class TestEdgeVariablesDerivedCorrectly:
126+
"""
127+
Post-v2 we are deriving
128+
129+
* EDGE_PORT
130+
* EDGE_PORT_HTTP
131+
* EDGE_BIND_HOST
132+
133+
from GATEWAY_LISTEN. We are also ensuring the configuration behaves
134+
well with LOCALSTACK_HOST, i.e. if LOCALSTACK_HOST is supplied and
135+
GATEWAY_LISTEN is not, then we should propagate LOCALSTACK_HOST configuration
136+
into GATEWAY_LISTEN.
137+
138+
Implementation note: monkeypatching the config module is hard, and causes
139+
tests run after these ones to import the wrong config. Instead, we test the
140+
function that populates the configuration variables.
141+
"""
142+
143+
@pytest.fixture
144+
def default_ip(self):
145+
if config.is_in_docker:
146+
return "0.0.0.0"
147+
else:
148+
return "127.0.0.1"
149+
150+
def test_defaults(self, default_ip):
151+
environment = {}
152+
(
153+
ls_host,
154+
gateway_listen,
155+
edge_bind_host,
156+
edge_port,
157+
edge_port_http,
158+
) = config.populate_legacy_edge_configuration(environment)
159+
160+
assert ls_host == "localhost.localstack.cloud:4566"
161+
assert gateway_listen == f"{default_ip}:4566"
162+
assert edge_port == 4566
163+
assert edge_port_http == 4566
164+
assert edge_bind_host == default_ip
165+
166+
def test_custom_hostname(self):
167+
environment = {"GATEWAY_LISTEN": "192.168.0.1"}
168+
(
169+
_,
170+
gateway_listen,
171+
edge_bind_host,
172+
edge_port,
173+
edge_port_http,
174+
) = config.populate_legacy_edge_configuration(environment)
175+
176+
assert gateway_listen == "192.168.0.1:4566"
177+
assert edge_port == 4566
178+
assert edge_port_http == 4566
179+
assert edge_bind_host == "192.168.0.1"
180+
181+
def test_custom_port(self, default_ip):
182+
environment = {"GATEWAY_LISTEN": ":9999"}
183+
(
184+
_,
185+
gateway_listen,
186+
edge_bind_host,
187+
edge_port,
188+
edge_port_http,
189+
) = config.populate_legacy_edge_configuration(environment)
190+
191+
assert gateway_listen == f"{default_ip}:9999"
192+
assert edge_port == 9999
193+
assert edge_port_http == 9999
194+
assert edge_bind_host == default_ip
195+
196+
def test_custom_host_and_port(self):
197+
environment = {"GATEWAY_LISTEN": "192.168.0.1:9999"}
198+
(
199+
_,
200+
gateway_listen,
201+
edge_bind_host,
202+
edge_port,
203+
edge_port_http,
204+
) = config.populate_legacy_edge_configuration(environment)
205+
206+
assert gateway_listen == "192.168.0.1:9999"
207+
assert edge_port == 9999
208+
assert edge_port_http == 9999
209+
assert edge_bind_host == "192.168.0.1"
210+
211+
def test_localstack_host_overrides_edge_variables(self, default_ip):
212+
environment = {"LOCALSTACK_HOST": "hostname:9999"}
213+
(
214+
ls_host,
215+
gateway_listen,
216+
edge_bind_host,
217+
edge_port,
218+
edge_port_http,
219+
) = config.populate_legacy_edge_configuration(environment)
220+
221+
assert ls_host == "hostname:9999"
222+
assert gateway_listen == f"{default_ip}:9999"
223+
assert edge_port == 9999
224+
assert edge_port_http == 9999
225+
assert edge_bind_host == default_ip
226+
227+
def test_gateway_listen_multiple_addresses(self):
228+
environment = {"GATEWAY_LISTEN": "0.0.0.0:9999,0.0.0.0:443"}
229+
(
230+
_,
231+
gateway_listen,
232+
edge_bind_host,
233+
edge_port,
234+
edge_port_http,
235+
) = config.populate_legacy_edge_configuration(environment)
236+
237+
assert gateway_listen == "0.0.0.0:9999,0.0.0.0:443"
238+
# take the first value
239+
assert edge_port == 9999
240+
assert edge_port_http == 9999
241+
assert edge_bind_host == "0.0.0.0"
242+
243+
@pytest.mark.parametrize(
244+
"input,hosts_and_ports",
245+
[
246+
("0.0.0.0:9999", [("0.0.0.0", 9999)]),
247+
(
248+
"0.0.0.0:9999,127.0.0.1:443",
249+
[
250+
("0.0.0.0", 9999),
251+
("127.0.0.1", 443),
252+
],
253+
),
254+
(
255+
"0.0.0.0:9999,127.0.0.1:443",
256+
[
257+
("0.0.0.0", 9999),
258+
("127.0.0.1", 443),
259+
],
260+
),
261+
],
262+
)
263+
def test_gateway_listen_parsed(self, input, hosts_and_ports):
264+
res = config.get_gateway_listen(input)
265+
266+
expected = [config.HostAndPort(host=host, port=port) for (host, port) in hosts_and_ports]
267+
assert res == expected
268+
269+
def test_legacy_variables_override_if_given(self, default_ip):
270+
environment = {
271+
"EDGE_BIND_HOST": "192.168.0.1",
272+
"EDGE_PORT": "10101",
273+
"EDGE_PORT_HTTP": "20202",
274+
}
275+
(
276+
_,
277+
gateway_listen,
278+
edge_bind_host,
279+
edge_port,
280+
edge_port_http,
281+
) = config.populate_legacy_edge_configuration(environment)
282+
283+
assert gateway_listen == f"{default_ip}:4566"
284+
assert edge_bind_host == "192.168.0.1"
285+
assert edge_port == 10101
286+
assert edge_port_http == 20202

0 commit comments

Comments
 (0)
0