mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 14:03:02 +00:00
add API endpoint for services
This commit is contained in:
@@ -312,6 +312,30 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: The users have been removed from the group
|
description: The users have been removed from the group
|
||||||
|
/services:
|
||||||
|
get:
|
||||||
|
summary: List services
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The service list
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/Service'
|
||||||
|
/services/{name}:
|
||||||
|
get:
|
||||||
|
summary: Get a service by name
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
description: service name
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The Service model
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Service'
|
||||||
/proxy:
|
/proxy:
|
||||||
get:
|
get:
|
||||||
summary: Get the proxy's routing table
|
summary: Get the proxy's routing table
|
||||||
@@ -436,3 +460,26 @@ definitions:
|
|||||||
description: The names of users who are members of this group
|
description: The names of users who are members of this group
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
Service:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The service's name
|
||||||
|
admin:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the service is an admin
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: The internal url where the service is running
|
||||||
|
prefix:
|
||||||
|
type: string
|
||||||
|
description: The proxied URL prefix to the service's url
|
||||||
|
pid:
|
||||||
|
type: number
|
||||||
|
description: The PID of the service process (if managed)
|
||||||
|
command:
|
||||||
|
type: array
|
||||||
|
description: The command used to start the service (if managed)
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
@@ -54,7 +54,7 @@ JUPYTERHUB_SERVICE_NAME: the name of the service ('cull-idle' above)
|
|||||||
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
||||||
JUPYTERHUB_API_URL: URL for the JupyterHub API (http://127.0.0.1:8080/hub/api)
|
JUPYTERHUB_API_URL: URL for the JupyterHub API (http://127.0.0.1:8080/hub/api)
|
||||||
JUPYTERHUB_BASE_URL: Base URL of the Hub (https://mydomain[:port]/)
|
JUPYTERHUB_BASE_URL: Base URL of the Hub (https://mydomain[:port]/)
|
||||||
JUPYTERHUB_SERVICE_PATH: Base path of this service (/service/cull-idle/)
|
JUPYTERHUB_SERVICE_PREFIX: URL path prefix of this service (/services/cull-idle/)
|
||||||
```
|
```
|
||||||
|
|
||||||
## External services
|
## External services
|
||||||
|
@@ -1,11 +1,6 @@
|
|||||||
from .base import *
|
from .base import *
|
||||||
from .auth import *
|
from . import auth, hub, proxy, users, groups, services
|
||||||
from .hub import *
|
|
||||||
from .proxy import *
|
|
||||||
from .users import *
|
|
||||||
from .groups import *
|
|
||||||
from . import auth, hub, proxy, users
|
|
||||||
|
|
||||||
default_handlers = []
|
default_handlers = []
|
||||||
for mod in (auth, hub, proxy, users, groups):
|
for mod in (auth, hub, proxy, users, groups, services):
|
||||||
default_handlers.extend(mod.default_handlers)
|
default_handlers.extend(mod.default_handlers)
|
||||||
|
64
jupyterhub/apihandlers/services.py
Normal file
64
jupyterhub/apihandlers/services.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""Service handlers
|
||||||
|
|
||||||
|
Currently GET-only, no actions can be taken to modify services.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tornado import web
|
||||||
|
|
||||||
|
from .. import orm
|
||||||
|
from ..utils import admin_only
|
||||||
|
from .base import APIHandler
|
||||||
|
|
||||||
|
def service_model(service):
|
||||||
|
"""Produce the model for a service"""
|
||||||
|
return {
|
||||||
|
'name': service.name,
|
||||||
|
'admin': service.admin,
|
||||||
|
'url': service.url,
|
||||||
|
'prefix': service.server.base_url if service.server else '',
|
||||||
|
'command': service.command,
|
||||||
|
'pid': service.proc.pid if service.proc else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceListAPIHandler(APIHandler):
|
||||||
|
@admin_only
|
||||||
|
def get(self):
|
||||||
|
data = {name: service_model(service) for name, service in self.services.items()}
|
||||||
|
self.write(json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
def admin_or_self(method):
|
||||||
|
"""Decorator for restricting access to either the target service or admin"""
|
||||||
|
def m(self, name):
|
||||||
|
current = self.get_current_user()
|
||||||
|
if current is None:
|
||||||
|
raise web.HTTPError(403)
|
||||||
|
if not current.admin:
|
||||||
|
# not admin, maybe self
|
||||||
|
if not isinstance(current, orm.Service):
|
||||||
|
raise web.HTTPError(403)
|
||||||
|
if current.name != name:
|
||||||
|
raise web.HTTPError(403)
|
||||||
|
# raise 404 if not found
|
||||||
|
if name not in self.services:
|
||||||
|
raise web.HTTPError(404)
|
||||||
|
return method(self, name)
|
||||||
|
return m
|
||||||
|
|
||||||
|
class ServiceAPIHandler(APIHandler):
|
||||||
|
|
||||||
|
@admin_or_self
|
||||||
|
def get(self, name):
|
||||||
|
service = self.services[name]
|
||||||
|
self.write(json.dumps(service_model(service)))
|
||||||
|
|
||||||
|
|
||||||
|
default_handlers = [
|
||||||
|
(r"/api/services", ServiceListAPIHandler),
|
||||||
|
(r"/api/services/([^/]+)", ServiceAPIHandler),
|
||||||
|
]
|
@@ -968,7 +968,7 @@ class JupyterHub(Application):
|
|||||||
self._add_tokens(self.api_tokens, kind='user')
|
self._add_tokens(self.api_tokens, kind='user')
|
||||||
|
|
||||||
def init_services(self):
|
def init_services(self):
|
||||||
self._service_map = {}
|
self._service_map.clear()
|
||||||
if self.domain:
|
if self.domain:
|
||||||
domain = 'services.' + self.domain
|
domain = 'services.' + self.domain
|
||||||
parsed = urlparse(self.subdomain_host)
|
parsed = urlparse(self.subdomain_host)
|
||||||
|
@@ -181,10 +181,10 @@ class Service(LoggingConfigurable):
|
|||||||
|
|
||||||
domain = Unicode()
|
domain = Unicode()
|
||||||
host = Unicode()
|
host = Unicode()
|
||||||
|
proc = Any()
|
||||||
|
|
||||||
# handles on globals:
|
# handles on globals:
|
||||||
proxy = Any()
|
proxy = Any()
|
||||||
|
|
||||||
base_url = Unicode()
|
base_url = Unicode()
|
||||||
db = Any()
|
db = Any()
|
||||||
orm = Any()
|
orm = Any()
|
||||||
@@ -225,7 +225,7 @@ class Service(LoggingConfigurable):
|
|||||||
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
||||||
env['JUPYTERHUB_API_URL'] = self.hub_api_url
|
env['JUPYTERHUB_API_URL'] = self.hub_api_url
|
||||||
env['JUPYTERHUB_BASE_URL'] = self.base_url
|
env['JUPYTERHUB_BASE_URL'] = self.base_url
|
||||||
env['JUPYTERHUB_SERVICE_PATH'] = self.server.base_url
|
env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
|
||||||
env['JUPYTERHUB_SERVICE_URL'] = self.url
|
env['JUPYTERHUB_SERVICE_URL'] = self.url
|
||||||
|
|
||||||
self.spawner = _ServiceSpawner(
|
self.spawner = _ServiceSpawner(
|
||||||
|
@@ -5,14 +5,19 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
|
from subprocess import TimeoutExpired
|
||||||
from pytest import fixture
|
import time
|
||||||
|
from unittest import mock
|
||||||
|
from pytest import fixture, yield_fixture, raises
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from ..utils import random_port
|
||||||
|
|
||||||
from .mocking import MockHub
|
from .mocking import MockHub
|
||||||
|
from .test_services import mockservice_cmd
|
||||||
|
|
||||||
|
import jupyterhub.services.service
|
||||||
|
|
||||||
# global db session object
|
# global db session object
|
||||||
_db = None
|
_db = None
|
||||||
@@ -53,3 +58,34 @@ def app(request):
|
|||||||
app.stop()
|
app.stop()
|
||||||
request.addfinalizer(fin)
|
request.addfinalizer(fin)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# mock services for testing.
|
||||||
|
# Shorter intervals, etc.
|
||||||
|
class MockServiceSpawner(jupyterhub.services.service._ServiceSpawner):
|
||||||
|
poll_interval = 1
|
||||||
|
|
||||||
|
|
||||||
|
@yield_fixture
|
||||||
|
def mockservice(request, app):
|
||||||
|
name = 'mock-service'
|
||||||
|
with mock.patch.object(jupyterhub.services.service, '_ServiceSpawner', MockServiceSpawner):
|
||||||
|
app.services = [{
|
||||||
|
'name': name,
|
||||||
|
'command': mockservice_cmd,
|
||||||
|
'url': 'http://127.0.0.1:%i' % random_port(),
|
||||||
|
'admin': True,
|
||||||
|
}]
|
||||||
|
app.init_services()
|
||||||
|
app.io_loop.add_callback(app.proxy.add_all_services, app._service_map)
|
||||||
|
assert name in app._service_map
|
||||||
|
service = app._service_map[name]
|
||||||
|
app.io_loop.add_callback(service.start)
|
||||||
|
request.addfinalizer(service.stop)
|
||||||
|
for i in range(20):
|
||||||
|
if not getattr(service, 'proc', False):
|
||||||
|
time.sleep(0.2)
|
||||||
|
# ensure process finishes starting
|
||||||
|
with raises(TimeoutExpired):
|
||||||
|
service.proc.wait(1)
|
||||||
|
yield service
|
||||||
|
@@ -6,6 +6,7 @@ from queue import Queue
|
|||||||
import sys
|
import sys
|
||||||
from urllib.parse import urlparse, quote
|
from urllib.parse import urlparse, quote
|
||||||
|
|
||||||
|
from pytest import mark
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
@@ -155,6 +156,7 @@ def test_referer_check(app, io_loop):
|
|||||||
|
|
||||||
# user API tests
|
# user API tests
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_get_users(app):
|
def test_get_users(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
r = api_request(app, 'users')
|
r = api_request(app, 'users')
|
||||||
@@ -185,6 +187,8 @@ def test_get_users(app):
|
|||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_user(app):
|
def test_add_user(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'newuser'
|
name = 'newuser'
|
||||||
@@ -196,6 +200,7 @@ def test_add_user(app):
|
|||||||
assert not user.admin
|
assert not user.admin
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_get_user(app):
|
def test_get_user(app):
|
||||||
name = 'user'
|
name = 'user'
|
||||||
r = api_request(app, 'users', name)
|
r = api_request(app, 'users', name)
|
||||||
@@ -211,6 +216,7 @@ def test_get_user(app):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_multi_user_bad(app):
|
def test_add_multi_user_bad(app):
|
||||||
r = api_request(app, 'users', method='post')
|
r = api_request(app, 'users', method='post')
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
@@ -220,6 +226,7 @@ def test_add_multi_user_bad(app):
|
|||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_multi_user_invalid(app):
|
def test_add_multi_user_invalid(app):
|
||||||
app.authenticator.username_pattern = r'w.*'
|
app.authenticator.username_pattern = r'w.*'
|
||||||
r = api_request(app, 'users', method='post',
|
r = api_request(app, 'users', method='post',
|
||||||
@@ -230,6 +237,7 @@ def test_add_multi_user_invalid(app):
|
|||||||
assert r.json()['message'] == 'Invalid usernames: andrew, tara'
|
assert r.json()['message'] == 'Invalid usernames: andrew, tara'
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_multi_user(app):
|
def test_add_multi_user(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
names = ['a', 'b']
|
names = ['a', 'b']
|
||||||
@@ -265,6 +273,7 @@ def test_add_multi_user(app):
|
|||||||
assert r_names == ['ab']
|
assert r_names == ['ab']
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_multi_user_admin(app):
|
def test_add_multi_user_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
names = ['c', 'd']
|
names = ['c', 'd']
|
||||||
@@ -283,6 +292,7 @@ def test_add_multi_user_admin(app):
|
|||||||
assert user.admin
|
assert user.admin
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_user_bad(app):
|
def test_add_user_bad(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'dne_newuser'
|
name = 'dne_newuser'
|
||||||
@@ -291,6 +301,8 @@ def test_add_user_bad(app):
|
|||||||
user = find_user(db, name)
|
user = find_user(db, name)
|
||||||
assert user is None
|
assert user is None
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_add_admin(app):
|
def test_add_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'newadmin'
|
name = 'newadmin'
|
||||||
@@ -304,6 +316,7 @@ def test_add_admin(app):
|
|||||||
assert user.admin
|
assert user.admin
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_delete_user(app):
|
def test_delete_user(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
mal = add_user(db, name='mal')
|
mal = add_user(db, name='mal')
|
||||||
@@ -311,6 +324,7 @@ def test_delete_user(app):
|
|||||||
assert r.status_code == 204
|
assert r.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
@mark.user
|
||||||
def test_make_admin(app):
|
def test_make_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'admin2'
|
name = 'admin2'
|
||||||
@@ -542,6 +556,7 @@ def test_bad_get_token(app):
|
|||||||
|
|
||||||
# group API tests
|
# group API tests
|
||||||
|
|
||||||
|
@mark.group
|
||||||
def test_groups_list(app):
|
def test_groups_list(app):
|
||||||
r = api_request(app, 'groups')
|
r = api_request(app, 'groups')
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -562,6 +577,7 @@ def test_groups_list(app):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
@mark.group
|
||||||
def test_group_get(app):
|
def test_group_get(app):
|
||||||
group = orm.Group.find(app.db, name='alphaflight')
|
group = orm.Group.find(app.db, name='alphaflight')
|
||||||
user = add_user(app.db, app=app, name='sasquatch')
|
user = add_user(app.db, app=app, name='sasquatch')
|
||||||
@@ -580,6 +596,7 @@ def test_group_get(app):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mark.group
|
||||||
def test_group_create_delete(app):
|
def test_group_create_delete(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
r = api_request(app, 'groups/runaways', method='delete')
|
r = api_request(app, 'groups/runaways', method='delete')
|
||||||
@@ -613,9 +630,9 @@ def test_group_create_delete(app):
|
|||||||
# delete nonexistant gives 404
|
# delete nonexistant gives 404
|
||||||
r = api_request(app, 'groups/omegaflight', method='delete')
|
r = api_request(app, 'groups/omegaflight', method='delete')
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@mark.group
|
||||||
def test_group_add_users(app):
|
def test_group_add_users(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
# must specify users
|
# must specify users
|
||||||
@@ -637,6 +654,7 @@ def test_group_add_users(app):
|
|||||||
assert sorted([ u.name for u in group.users ]) == sorted(names)
|
assert sorted([ u.name for u in group.users ]) == sorted(names)
|
||||||
|
|
||||||
|
|
||||||
|
@mark.group
|
||||||
def test_group_delete_users(app):
|
def test_group_delete_users(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
# must specify users
|
# must specify users
|
||||||
@@ -659,6 +677,61 @@ def test_group_delete_users(app):
|
|||||||
assert sorted([ u.name for u in group.users ]) == sorted(names[2:])
|
assert sorted([ u.name for u in group.users ]) == sorted(names[2:])
|
||||||
|
|
||||||
|
|
||||||
|
# service API
|
||||||
|
@mark.services
|
||||||
|
def test_get_services(app, mockservice):
|
||||||
|
db = app.db
|
||||||
|
r = api_request(app, 'services')
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
services = r.json()
|
||||||
|
assert services == {
|
||||||
|
'mock-service': {
|
||||||
|
'name': 'mock-service',
|
||||||
|
'admin': True,
|
||||||
|
'command': mockservice.command,
|
||||||
|
'pid': mockservice.proc.pid,
|
||||||
|
'prefix': mockservice.server.base_url,
|
||||||
|
'url': mockservice.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r = api_request(app, 'services',
|
||||||
|
headers=auth_header(db, 'user'),
|
||||||
|
)
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
def test_get_service(app, mockservice):
|
||||||
|
db = app.db
|
||||||
|
r = api_request(app, 'services/%s' % mockservice.name)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
service = r.json()
|
||||||
|
assert service == {
|
||||||
|
'name': 'mock-service',
|
||||||
|
'admin': True,
|
||||||
|
'command': mockservice.command,
|
||||||
|
'pid': mockservice.proc.pid,
|
||||||
|
'prefix': mockservice.server.base_url,
|
||||||
|
'url': mockservice.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = api_request(app, 'services/%s' % mockservice.name,
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token %s' % mockservice.api_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
r = api_request(app, 'services/%s' % mockservice.name,
|
||||||
|
headers=auth_header(db, 'user'),
|
||||||
|
)
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_root_api(app):
|
def test_root_api(app):
|
||||||
base_url = app.hub.server.url
|
base_url = app.hub.server.url
|
||||||
url = ujoin(base_url, 'api')
|
url = ujoin(base_url, 'api')
|
||||||
|
@@ -3,25 +3,17 @@
|
|||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
from subprocess import Popen, TimeoutExpired
|
from subprocess import Popen
|
||||||
import sys
|
import sys
|
||||||
from threading import Event
|
from threading import Event
|
||||||
import time
|
import time
|
||||||
try:
|
|
||||||
from unittest import mock
|
|
||||||
except ImportError:
|
|
||||||
import mock
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import requests
|
import requests
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
|
|
||||||
import jupyterhub.services.service
|
|
||||||
from .mocking import public_url
|
from .mocking import public_url
|
||||||
from .test_pages import get_page
|
|
||||||
from ..utils import url_path_join, wait_for_http_server
|
from ..utils import url_path_join, wait_for_http_server
|
||||||
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -30,6 +22,7 @@ mockservice_cmd = [sys.executable, mockservice_py]
|
|||||||
|
|
||||||
from ..utils import random_port
|
from ..utils import random_port
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def external_service(app, name='mockservice'):
|
def external_service(app, name='mockservice'):
|
||||||
env = {
|
env = {
|
||||||
@@ -46,36 +39,6 @@ def external_service(app, name='mockservice'):
|
|||||||
p.terminate()
|
p.terminate()
|
||||||
|
|
||||||
|
|
||||||
# mock services for testing.
|
|
||||||
# Shorter intervals, etc.
|
|
||||||
class MockServiceSpawner(jupyterhub.services.service._ServiceSpawner):
|
|
||||||
poll_interval = 1
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def mockservice(request, app):
|
|
||||||
name = 'mock-service'
|
|
||||||
with mock.patch.object(jupyterhub.services.service, '_ServiceSpawner', MockServiceSpawner):
|
|
||||||
app.services = [{
|
|
||||||
'name': name,
|
|
||||||
'command': mockservice_cmd,
|
|
||||||
'url': 'http://127.0.0.1:%i' % random_port(),
|
|
||||||
'admin': True,
|
|
||||||
}]
|
|
||||||
app.init_services()
|
|
||||||
app.io_loop.add_callback(app.proxy.add_all_services, app._service_map)
|
|
||||||
assert name in app._service_map
|
|
||||||
service = app._service_map[name]
|
|
||||||
app.io_loop.add_callback(service.start)
|
|
||||||
request.addfinalizer(service.stop)
|
|
||||||
for i in range(20):
|
|
||||||
if not getattr(service, 'proc', False):
|
|
||||||
time.sleep(0.2)
|
|
||||||
# ensure process finishes starting
|
|
||||||
with pytest.raises(TimeoutExpired):
|
|
||||||
service.proc.wait(1)
|
|
||||||
yield service
|
|
||||||
|
|
||||||
|
|
||||||
def test_managed_service(app, mockservice):
|
def test_managed_service(app, mockservice):
|
||||||
service = mockservice
|
service = mockservice
|
||||||
proc = service.proc
|
proc = service.proc
|
||||||
|
Reference in New Issue
Block a user