Skip to content
Permalink
Browse files

Basic workflow skeleton

  • Loading branch information...
GeorgianaElena committed Dec 4, 2018
1 parent dfdaf52 commit 5a00ae80e2c03779156550eac3fe3f2d05a9c27e
Showing with 131 additions and 20 deletions.
  1. +1 −1 git-hooks/README.md
  2. +110 −10 jupyterhub_traefik_proxy/proxy.py
  3. +3 −1 tests/conftest.py
  4. +17 −8 tests/test_proxy.py
@@ -7,6 +7,6 @@ so you don't have to worry about code formatting.

Install it with:

./hooks/install
./git-hooks/install

from the root of the repo.
@@ -19,13 +19,56 @@
# Distributed under the terms of the Modified BSD License.

import asyncio
import json
import etcd3

from jupyterhub.proxy import Proxy
from urllib.parse import urlparse
from subprocess import Popen

from jupyterhub.proxy import Proxy

class TraefikProxy(Proxy):
"""JupyterHub Proxy implementation using traefik"""

client = etcd3.client("127.0.0.1", 2379)
traefik_port = 8000
traefik = None
prefix = "/traefik"
jupyterhub_prefix = "/jupyterhub"

def _setup_etcd(self):
print("Seting up etcd...")
self.client.put(self.prefix + '/debug', 'true')
self.client.put(self.prefix + '/defaultentrypoints/0', 'http')
self.client.put(self.prefix + '/entrypoints/http/address', ':8000')
self.client.put(self.prefix + '/api/dashboard', 'true')
self.client.put(self.prefix + '/api/entrypoint', 'http')
self.client.put(self.prefix + '/loglevel', 'ERROR')
self.client.put(self.prefix + '/etcd/endpoint', '127.0.0.1:2379')
self.client.put(self.prefix + '/etcd/prefix', self.prefix)
self.client.put(self.prefix + '/etcd/useapiv3', 'true')
self.client.put(self.prefix + '/etcd/watch', 'true')

def _create_backend_alias_from_url(self, url):
target = urlparse(url)
return "jupyterhub_backend_" + target.netloc

def _create_frontend_alias_from_url(self, url):
target = urlparse(url)
return "jupyterhub_frontend_" + target.netloc

def _create_backend_url_path(self, backend_alias):
return self.prefix + '/backends/' + backend_alias + "/servers/server1/url"

def _create_backend_weight_path(self, backend_alias):
return self.prefix + '/backends/' + backend_alias + "/servers/server1/weight"

def _create_frontend_backend_path(self, frontend_alias):
return self.prefix + '/frontends/' + frontend_alias + "/backend"

def _create_frontend_rule_path(self, frontend_alias):
return self.prefix + '/frontends/' + frontend_alias + "/routes/test/rule"

async def start(self):
"""Start the proxy.
@@ -34,7 +77,10 @@ class TraefikProxy(Proxy):
**Subclasses must define this method**
if the proxy is to be started by the Hub
"""
raise NotImplementedError()

#TODO check if there is another proxy process running
self.traefik = Popen(["traefik", "--etcd", "--etcd.useapiv3=true"], stdout=None)
# raise NotImplementedError()

async def stop(self):
"""Stop the proxy.
@@ -44,7 +90,11 @@ class TraefikProxy(Proxy):
**Subclasses must define this method**
if the proxy is to be started by the Hub
"""
raise NotImplementedError()
# raise NotImplementedError()

print(self.traefik.kill())
print(self.traefik.wait())


async def add_route(self, routespec, target, data):
"""Add a route to the proxy.
@@ -64,14 +114,41 @@ class TraefikProxy(Proxy):
The proxy implementation should also have a way to associate the fact that a
route came from JupyterHub.
"""
raise NotImplementedError()

backend_alias = self._create_backend_alias_from_url(target)
backend_url_path = self._create_backend_url_path(backend_alias)
backend_weight_path = self._create_backend_weight_path(backend_alias)
frontend_alias = self._create_frontend_alias_from_url(target)
frontend_backend_path = self._create_frontend_backend_path(frontend_alias)
frontend_rule_path = self._create_frontend_rule_path(frontend_alias)

self.client.put(self.jupyterhub_prefix + routespec, target)
# To be able to delete the route when routespec is provided
encoded_data = data=json.dumps(data)
self.client.put(target, encoded_data)
# Store the data dict passed in by JupyterHub
self.client.put(backend_url_path, target)
self.client.put(backend_weight_path, "1")
self.client.put(frontend_backend_path, backend_alias)
self.client.put(frontend_rule_path, "PathPrefix:" + routespec)


async def delete_route(self, routespec):
"""Delete a route with a given routespec if it exists.
**Subclasses must define this method**
"""
raise NotImplementedError()
value, _ = self.client.get(self.jupyterhub_prefix + routespec)
target = value.decode()

self.client.delete(self.jupyterhub_prefix + routespec)
self.client.delete(target)

backend_alias = self._create_backend_alias_from_url(target)
frontend_alias = self._create_frontend_alias_from_url(target)

self.client.delete_prefix(self.prefix + '/backends/' + backend_alias)
self.client.delete_prefix(self.prefix + '/frontends/' + frontend_alias)

async def get_all_routes(self):
"""Fetch and return all the routes associated by JupyterHub from the
@@ -88,7 +165,22 @@ class TraefikProxy(Proxy):
'data': the attached data dict for this route (as specified in add_route)
}
"""
raise NotImplementedError()
result = {}
routes = self.client.get_prefix(self.jupyterhub_prefix)
for value, metadata in routes:
routespec = metadata.key.decode().replace(self.jupyterhub_prefix, "")
target = value.decode()

value, _ = self.client.get(target)
data = value.decode()
partial_res = {
"routespec": routespec,
"target": target,
"data": data
}

result[routespec] = partial_res
return result

async def get_route(self, routespec):
"""Return the route info for a given routespec.
@@ -110,7 +202,15 @@ class TraefikProxy(Proxy):
None: if there are no routes matching the given routespec
"""
# default implementation relies on get_all_routes
routespec = self.validate_routespec(routespec)
routes = await self.get_all_routes()
return routes.get(routespec)

value, _ = self.client.get(self.jupyterhub_prefix + routespec)
target = value.decode()
value, _ = self.client.get(target)
data = value.decode()
result = {
"routespec": routespec,
"target": target,
"data": data
}

return result
@@ -6,8 +6,10 @@


@pytest.fixture
def proxy():
async def proxy():
"""Fixture returning a configured Traefik Proxy"""
# TODO: set up the proxy
proxy = TraefikProxy()
proxy._setup_etcd()
await proxy.start()
yield proxy
@@ -1,29 +1,38 @@
"""Tests for the base traefik proxy"""

import pytest
import json

# mark all tests in this file as asyncio
pytestmark = pytest.mark.asyncio


async def test_add_route(proxy):
with pytest.raises(NotImplementedError):
route = await proxy.add_route("/prefix", "http://127.0.0.1:9999", {})
# with pytest.raises(NotImplementedError):
await proxy.add_route("/prefix", "http://127.0.0.1:99", {"test": "test0"})
await proxy.add_route("/prefix0", "http://127.0.0.1:1000", {"test": "test0"})
await proxy.stop()
# print("route is " + route)
# TODO: test the route


async def test_get_all_routes(proxy):
with pytest.raises(NotImplementedError):
routes = await proxy.get_all_routes()
# with pytest.raises(NotImplementedError):
routes = await proxy.get_all_routes()
print(json.dumps(routes, sort_keys=True, indent=4, separators=(',', ': ')))
await proxy.stop()
# TODO: test the routes


async def test_delete_route(proxy):
with pytest.raises(NotImplementedError):
await proxy.delete_route("/prefix")
# with pytest.raises(NotImplementedError):
await proxy.delete_route("/prefix0")
await proxy.stop()


async def test_get_route(proxy):
with pytest.raises(NotImplementedError):
route = await proxy.get_route("/prefix")
# with pytest.raises(NotImplementedError):
route = await proxy.get_route("/prefix")
print(json.dumps(route, sort_keys=True, indent=4, separators=(',', ': ')))
await proxy.stop()
# TODO: test the route

0 comments on commit 5a00ae8

Please sign in to comment.
You can’t perform that action at this time.