Skip to content

Commit bec86ce

Browse files
authored
Merge pull request #184 from minrk/rm-autodiscover
Make traefik_entrypoint name explicit
2 parents ab4b05c + cd8bc0d commit bec86ce

File tree

6 files changed

+96
-67
lines changed

6 files changed

+96
-67
lines changed

jupyterhub_traefik_proxy/proxy.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import asyncio
2222
import json
2323
import os
24+
import ssl
2425
from os.path import abspath
2526
from subprocess import Popen, TimeoutExpired
2627
from urllib.parse import urlparse, urlunparse
@@ -105,6 +106,14 @@ def __init__(self, **kwargs):
105106
if trait.metadata.get("deprecated_in"):
106107
self.observe(self._deprecated_trait, name)
107108
super().__init__(**kwargs)
109+
# not calling .start, make sure we load our initial dynamic config
110+
# and that our static config matches what we expect
111+
# can't await here because we're not async,
112+
# so await in our Proxy methods
113+
if not self.should_start:
114+
self._start_future = asyncio.ensure_future(self._start_external())
115+
else:
116+
self._start_future = None
108117

109118
static_config = Dict()
110119
dynamic_config = Dict()
@@ -319,6 +328,9 @@ async def _check_traefik_static_conf_ready():
319328
"""Check if traefik loaded its static configuration yet"""
320329
try:
321330
await self._traefik_api_request("/api/overview")
331+
await self._traefik_api_request(
332+
f"/api/entrypoints/{self.traefik_entrypoint}"
333+
)
322334
except ConnectionRefusedError:
323335
self.log.debug(
324336
f"Connection Refused waiting for traefik at {self.traefik_api_url}. It's probably starting up..."
@@ -331,13 +343,24 @@ async def _check_traefik_static_conf_ready():
331343
)
332344
return False
333345
if e.code == 404:
334-
self.log.debug(
335-
f"traefik api at {e.response.request.url} overview not ready yet"
336-
)
346+
if "/entrypoints/" in e.response.request.url:
347+
self.log.warning(
348+
f"c.{self.__class__.__name__}.traefik_entrypoint={self.traefik_entrypoint!r} not found in traefik. Is it correct?"
349+
)
350+
else:
351+
self.log.debug(
352+
f"traefik api at {e.response.request.url} overview not ready yet"
353+
)
337354
return False
338355
# unexpected
339356
self.log.error(f"Error checking for traefik static configuration {e}")
340357
return False
358+
except (OSError, ssl.SSLError) as e:
359+
# Can occur if SSL isn't set up yet
360+
self.log.warning(
361+
f"SSL Error checking for traefik static configuration: {e}"
362+
)
363+
return False
341364

342365
return True
343366

@@ -459,6 +482,15 @@ async def start(self):
459482
self._start_traefik()
460483
await self._wait_for_static_config()
461484

485+
async def _start_external(self):
486+
"""Startup function called when `not self.should_start`
487+
488+
Ensures dynamic config is setup and static config is loaded
489+
"""
490+
await self._setup_traefik_dynamic_config()
491+
await self._wait_for_static_config()
492+
self._start_future = None
493+
462494
async def stop(self):
463495
"""Stop the proxy.
464496
@@ -557,6 +589,8 @@ async def add_route(self, routespec, target, data):
557589
The proxy implementation should also have a way to associate the fact that a
558590
route came from JupyterHub.
559591
"""
592+
if self._start_future and not self._start_future.done():
593+
await self._start_future
560594
routespec = self.validate_routespec(routespec)
561595

562596
traefik_config, jupyterhub_config = self._dynamic_config_for_route(
@@ -613,6 +647,11 @@ async def _get_jupyterhub_dynamic_config(self):
613647
"""
614648
raise NotImplementedError()
615649

650+
async def check_routes(self, *args, **kwargs):
651+
if self._start_future and not self._start_future.done():
652+
await self._start_future
653+
return await super().check_routes(*args, **kwargs)
654+
616655
async def get_all_routes(self):
617656
"""Fetch and return all the routes associated by JupyterHub from the
618657
proxy.
@@ -628,6 +667,9 @@ async def get_all_routes(self):
628667
'data': the attached data dict for this route (as specified in add_route)
629668
}
630669
"""
670+
if self._start_future and not self._start_future.done():
671+
await self._start_future
672+
631673
jupyterhub_config = await self._get_jupyterhub_dynamic_config()
632674

633675
all_routes = {}

tests/config_files/dynamic_config/dynamic_conf.toml

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/config_files/traefik.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ level = "debug"
77
providersThrottleDuration = "0s"
88

99
[providers.file]
10-
directory = "./tests/config_files/dynamic_config"
10+
directory = "/tmp/jupyterhub-traefik-proxy-test"
1111
watch = true
1212

1313
[entryPoints.websecure]

tests/conftest.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from tempfile import TemporaryDirectory
1212

1313
import pytest
14+
import utils
1415
from consul.aio import Consul
1516
from jupyterhub.utils import exponential_backoff
1617
from traitlets.log import get_logger
@@ -78,6 +79,17 @@ def pytest_configure(config):
7879
config.addinivalue_line("markers", "slow: marks tests as slow.")
7980

8081

82+
@pytest.fixture
83+
def dynamic_config_dir():
84+
# matches traefik.toml
85+
path = Path("/tmp/jupyterhub-traefik-proxy-test")
86+
if path.exists():
87+
shutil.rmtree(path)
88+
path.mkdir()
89+
yield path
90+
shutil.rmtree(path)
91+
92+
8193
@pytest.fixture
8294
async def no_auth_consul_proxy(launch_consul):
8395
"""
@@ -181,9 +193,9 @@ def traitlets_log():
181193

182194
# There must be a way to parameterise this to run on both yaml and toml files?
183195
@pytest.fixture
184-
async def file_proxy_toml():
196+
async def file_proxy_toml(dynamic_config_dir):
185197
"""Fixture returning a configured TraefikFileProviderProxy"""
186-
dynamic_config_file = os.path.join(config_files, "dynamic_config", "rules.toml")
198+
dynamic_config_file = str(dynamic_config_dir / "rules.toml")
187199
static_config_file = "traefik.toml"
188200
proxy = _file_proxy(
189201
dynamic_config_file, static_config_file=static_config_file, should_start=True
@@ -194,8 +206,8 @@ async def file_proxy_toml():
194206

195207

196208
@pytest.fixture
197-
async def file_proxy_yaml():
198-
dynamic_config_file = os.path.join(config_files, "dynamic_config", "rules.yaml")
209+
async def file_proxy_yaml(dynamic_config_dir):
210+
dynamic_config_file = str(dynamic_config_dir / "rules.yaml")
199211
static_config_file = "traefik.yaml"
200212
proxy = _file_proxy(
201213
dynamic_config_file, static_config_file=static_config_file, should_start=True
@@ -218,16 +230,16 @@ def _file_proxy(dynamic_config_file, **kwargs):
218230

219231

220232
@pytest.fixture
221-
async def external_file_proxy_yaml(launch_traefik_file):
222-
dynamic_config_file = os.path.join(config_files, "dynamic_config", "rules.yaml")
233+
async def external_file_proxy_yaml(launch_traefik_file, dynamic_config_dir):
234+
dynamic_config_file = str(dynamic_config_dir / "rules.yaml")
223235
proxy = _file_proxy(dynamic_config_file, should_start=False)
224236
yield proxy
225237
os.remove(dynamic_config_file)
226238

227239

228240
@pytest.fixture
229-
async def external_file_proxy_toml(launch_traefik_file):
230-
dynamic_config_file = os.path.join(config_files, "dynamic_config", "rules.toml")
241+
async def external_file_proxy_toml(launch_traefik_file, dynamic_config_dir):
242+
dynamic_config_file = str(dynamic_config_dir / "rules.toml")
231243
proxy = _file_proxy(dynamic_config_file, should_start=False)
232244
yield proxy
233245
os.remove(dynamic_config_file)
@@ -277,6 +289,36 @@ async def auth_external_etcd_proxy(
277289
proxy.etcd.close()
278290

279291

292+
@pytest.fixture(
293+
params=[
294+
"no_auth_consul_proxy",
295+
"auth_consul_proxy",
296+
"no_auth_etcd_proxy",
297+
"auth_etcd_proxy",
298+
"file_proxy_toml",
299+
"file_proxy_yaml",
300+
"external_consul_proxy",
301+
"auth_external_consul_proxy",
302+
"external_etcd_proxy",
303+
"auth_external_etcd_proxy",
304+
"external_file_proxy_toml",
305+
"external_file_proxy_yaml",
306+
]
307+
)
308+
def proxy(request):
309+
"""Parameterized fixture to run all the tests with every proxy implementation"""
310+
proxy = request.getfixturevalue(request.param)
311+
# wait for public endpoint to be reachable
312+
asyncio.run(
313+
exponential_backoff(
314+
utils.check_host_up_http,
315+
f"Proxy public url {proxy.public_url} cannot be reached",
316+
url=proxy.public_url,
317+
)
318+
)
319+
return proxy
320+
321+
280322
#########################################################################
281323
# Fixtures for launching traefik, with each backend and with or without #
282324
# authentication #

tests/test_proxy.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -138,36 +138,6 @@ async def _launch_backends(n=1):
138138
proc.wait()
139139

140140

141-
@pytest.fixture(
142-
params=[
143-
"no_auth_consul_proxy",
144-
"auth_consul_proxy",
145-
"no_auth_etcd_proxy",
146-
"auth_etcd_proxy",
147-
"file_proxy_toml",
148-
"file_proxy_yaml",
149-
"external_consul_proxy",
150-
"auth_external_consul_proxy",
151-
"external_etcd_proxy",
152-
"auth_external_etcd_proxy",
153-
"external_file_proxy_toml",
154-
"external_file_proxy_yaml",
155-
]
156-
)
157-
def proxy(request):
158-
"""Parameterized fixture to run all the tests with every proxy implementation"""
159-
proxy = request.getfixturevalue(request.param)
160-
# wait for public endpoint to be reachable
161-
asyncio.run(
162-
exponential_backoff(
163-
utils.check_host_up_http,
164-
f"Proxy public url {proxy.public_url} cannot be reached",
165-
url=proxy.public_url,
166-
)
167-
)
168-
return proxy
169-
170-
171141
async def wait_for_services(urls):
172142
# Wait until traefik and the backend are ready
173143
await exponential_backoff(

tests/test_traefik_api_auth.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,6 @@
77
pytestmark = pytest.mark.asyncio
88

99

10-
@pytest.fixture(
11-
params=[
12-
"file_proxy_toml",
13-
"file_proxy_yaml",
14-
"no_auth_etcd_proxy",
15-
"auth_etcd_proxy",
16-
"no_auth_consul_proxy",
17-
"auth_consul_proxy",
18-
]
19-
)
20-
def proxy(request):
21-
return request.getfixturevalue(request.param)
22-
23-
2410
@pytest.mark.parametrize(
2511
"username, password, expected_rc",
2612
[("api_admin", "admin", 200), ("api_admin", "1234", 401), ("", "", 401)],

0 commit comments

Comments
 (0)