Skip to content

Commit 02ee300

Browse files
authored
Merge pull request #1 from yacchin1205/feature/grdm
[GRDM-31008] GRDM対応
2 parents d711bda + 6daf9c4 commit 02ee300

File tree

6 files changed

+389
-9
lines changed

6 files changed

+389
-9
lines changed

README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
1717
sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
1818
sudo apt update && sudo apt install -y docker-ce
1919

20+
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
21+
sudo apt update && sudo apt install -y nodejs libfuse-dev python3-dev libcurl4-openssl-dev libssl-dev
22+
sudo modprobe fuse
23+
2024
# pull the repo2docker image
21-
sudo docker pull quay.io/jupyterhub/repo2docker:main
25+
sudo docker pull gcr.io/nii-ap-ops/repo2docker:20220330
26+
sudo docker pull gcr.io/nii-ap-ops/rdmfs:20211221
2227

2328
# install TLJH
2429
curl https://raw.githubusercontent.com/jupyterhub/the-littlest-jupyterhub/master/bootstrap/bootstrap.py \
2530
| sudo python3 - \
2631
--admin test:test \
27-
--plugin git+https://github.com/plasmabio/tljh-repo2docker@master
32+
--plugin git+https://github.com/RCOSDP/CS-tljh-repo2docker.git@master
33+
34+
# fix to use the RCOSDP's one as binderhub package.
35+
sudo /opt/tljh/hub/bin/python -m pip install --upgrade git+https://github.com/RCOSDP/CS-binderhub.git
36+
# restart TLJH
37+
sudo systemctl restart jupyterhub
2838
```
2939

3040
Refer to [The Littlest JupyterHub documentation](http://tljh.jupyter.org/en/latest/topic/customizing-installer.html?highlight=plugins#installing-tljh-plugins)

setup.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
entry_points={"tljh": ["tljh_repo2docker = tljh_repo2docker"]},
77
packages=find_packages(),
88
include_package_data=True,
9-
install_requires=["dockerspawner~=12.1", "jupyter_client~=6.1", "aiodocker~=0.19"],
9+
install_requires=[
10+
"dockerspawner~=12.1", "jupyter_client~=6.1", "aiodocker~=0.19", "binderhub",
11+
],
12+
dependency_links = [
13+
"git+https://github.com/RCOSDP/CS-binderhub.git#egg=binderhub",
14+
],
1015
)

tljh_repo2docker/__init__.py

+220-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import os
22

33
from aiodocker import Docker
4+
from aiodocker.exceptions import DockerError
45
from dockerspawner import DockerSpawner
6+
from docker.errors import APIError
7+
from docker.types import Mount
58
from jinja2 import Environment, BaseLoader
69
from jupyter_client.localinterfaces import public_ips
710
from jupyterhub.handlers.static import CacheControlStaticFilesHandler
@@ -10,11 +13,15 @@
1013
from tljh.configurer import load_config
1114
from traitlets import Unicode
1215
from traitlets.config import Configurable
16+
from tornado import web
1317

1418
from .builder import BuildHandler
19+
from .launcher import LaunchHandler
1520
from .docker import list_images
1621
from .images import ImagesHandler
1722
from .logs import LogsHandler
23+
from .token import TokenStore
24+
1825

1926
# Default CPU period
2027
# See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler
@@ -92,6 +99,22 @@ class SpawnerMixin(Configurable):
9299
""",
93100
)
94101

102+
rdmfs_base_path = Unicode(
103+
config=True,
104+
help="""
105+
A base path for RDMFS.
106+
""",
107+
)
108+
109+
token_store_path = Unicode(
110+
config=True,
111+
help="""
112+
A dbpath of token_store.
113+
""",
114+
)
115+
116+
extra_mounts = None
117+
95118
async def list_images(self):
96119
"""
97120
Return the list of available images
@@ -155,19 +178,171 @@ async def set_limits(self):
155178
}
156179
)
157180

181+
async def set_extra_mounts(self):
182+
"""
183+
Prepare volume binds for GRDM
184+
"""
185+
imagename = self.user_options.get("image")
186+
async with Docker() as docker:
187+
image = await docker.images.inspect(imagename)
188+
189+
provider_prefix = image["ContainerConfig"]["Labels"].get(
190+
"tljh_repo2docker.opt.provider", None
191+
)
192+
if provider_prefix != 'rdm':
193+
return
194+
await self._set_rdm_mounts(image)
195+
196+
async def _set_rdm_mounts(self, image):
197+
repo = image["ContainerConfig"]["Labels"].get(
198+
"tljh_repo2docker.opt.repo", None
199+
)
200+
token_store = TokenStore(dbpath=self.token_store_path)
201+
repo_token = token_store.get(self.user, repo)
202+
if repo_token is None:
203+
raise web.HTTPError(
204+
400,
205+
"No repo_token for: %s" % (repo),
206+
)
207+
self.log.info("Preparing RDMFS... " + 'name=' + repr(self.user.name) + ', repo=' + repr(repo))
208+
mount_path = os.path.join(self.rdmfs_base_path, self.container_name)
209+
if not os.path.exists(mount_path):
210+
os.makedirs(mount_path)
211+
self.extra_mounts = [
212+
dict(type='bind', source=mount_path, target='/mnt', propagation='rshared'),
213+
]
214+
rdmfs_id = await self.get_rdmfs_object()
215+
if rdmfs_id is not None:
216+
await self.remove_object_by_id(rdmfs_id)
217+
rdmfs_id = await self.create_rdmfs_object({
218+
'RDM_NODE_ID': image["ContainerConfig"]["Labels"].get(
219+
"tljh_repo2docker.opt.user.rdm_node_id", None
220+
),
221+
'RDM_API_URL': image["ContainerConfig"]["Labels"].get(
222+
"tljh_repo2docker.opt.user.rdm_api_url", None
223+
),
224+
'RDM_TOKEN': repo_token,
225+
'MOUNT_PATH': '/mnt/rdm',
226+
})
227+
await self.start_object_by_id(rdmfs_id)
228+
229+
async def get_rdmfs_object(self):
230+
object_name = self.object_name + '_rdmfs'
231+
self.log.debug("Getting %s '%s'", self.object_type, object_name)
232+
try:
233+
async with Docker() as docker:
234+
obj = await docker.containers.get(object_name)
235+
return obj.id
236+
except DockerError as e:
237+
if e.status == 404:
238+
self.log.info(
239+
"%s '%s' is gone", self.object_type.title(), object_name
240+
)
241+
elif e.status == 500:
242+
self.log.info(
243+
"%s '%s' is on unhealthy node",
244+
self.object_type.title(),
245+
object_name,
246+
)
247+
else:
248+
raise
249+
return None
250+
251+
async def create_rdmfs_object(self, env):
252+
host_config = dict(
253+
Mounts=[
254+
{
255+
"Type": "bind",
256+
"Source": m['source'],
257+
"Target": "/mnt",
258+
"ReadOnly": False,
259+
"BindOptions": {
260+
"Propagation": "rshared",
261+
},
262+
}
263+
for m in (self.extra_mounts or [])
264+
],
265+
Privileged=True,
266+
)
267+
create_kwargs = dict(
268+
Image='gcr.io/nii-ap-ops/rdmfs:20211221',
269+
Env=[f'{k}={v}' for k, v in env.items()],
270+
AutoRemove=True,
271+
HostConfig=host_config,
272+
)
273+
async with Docker() as docker:
274+
obj = await docker.containers.create(
275+
create_kwargs,
276+
name=self.container_name + '_rdmfs',
277+
)
278+
return obj.id
279+
280+
async def start_object_by_id(self, object_id):
281+
async with Docker() as docker:
282+
obj = await docker.containers.get(object_id)
283+
await obj.start()
284+
285+
async def remove_object_by_id(self, object_id):
286+
self.log.info("Removing %s %s", self.object_type, object_id)
287+
try:
288+
async with Docker() as docker:
289+
obj = await docker.containers.get(object_id)
290+
desc = await obj.show()
291+
if 'State' in desc and desc['State']['Running']:
292+
self.log.info('terminating...')
293+
exec = await obj.exec(["/bin/sh","-c","xattr -w command terminate /mnt/rdm"])
294+
result = await exec.start(detach=True)
295+
self.log.info('terminated: {}'.format(result))
296+
else:
297+
self.log.info('deleting...')
298+
await obj.delete()
299+
except DockerError as e:
300+
if e.status == 409:
301+
self.log.debug(
302+
"Already removing %s: %s", self.object_type, object_id
303+
)
304+
elif e.status == 404:
305+
self.log.debug(
306+
"Already removed %s: %s", self.object_type, object_id
307+
)
308+
else:
309+
raise
310+
158311

159312
class Repo2DockerSpawner(SpawnerMixin, DockerSpawner):
160313
"""
161314
A custom spawner for using local Docker images built with tljh-repo2docker.
162315
"""
163316

317+
@property
318+
def mount_binds(self):
319+
base_mount_binds = super().mount_binds.copy()
320+
if self.extra_mounts is None:
321+
return base_mount_binds
322+
base_mount_binds += [Mount(**m) for m in self.extra_mounts]
323+
return base_mount_binds
324+
164325
async def start(self, *args, **kwargs):
165326
await self.set_limits()
327+
await self.set_extra_mounts()
166328
return await super().start(*args, **kwargs)
167329

330+
async def stop(self, *args, **kwargs):
331+
await super().stop(*args, **kwargs)
332+
rdmfs_id = await self.get_rdmfs_object()
333+
if rdmfs_id is None:
334+
return
335+
await self.remove_object_by_id(rdmfs_id)
336+
168337

169338
@hookimpl
170339
def tljh_custom_jupyterhub_config(c):
340+
from binderhub.repoproviders import (
341+
GitHubRepoProvider, GitRepoProvider, GitLabRepoProvider, GistRepoProvider,
342+
ZenodoProvider, FigshareProvider, HydroshareProvider, DataverseProvider,
343+
RDMProvider, WEKO3Provider,
344+
)
345+
171346
# hub
172347
c.JupyterHub.hub_ip = public_ips()[0]
173348
c.JupyterHub.cleanup_servers = False
@@ -178,10 +353,14 @@ def tljh_custom_jupyterhub_config(c):
178353
0, os.path.join(os.path.dirname(__file__), "templates")
179354
)
180355

356+
token_store_path = '/opt/tljh/state/repo2docker.sqlite'
357+
181358
# spawner
182359
c.DockerSpawner.cmd = ["jupyterhub-singleuser"]
183360
c.DockerSpawner.pull_policy = "Never"
184361
c.DockerSpawner.remove = True
362+
c.Repo2DockerSpawner.rdmfs_base_path = '/opt/tljh/repo2docker/volumes'
363+
c.Repo2DockerSpawner.token_store_path = token_store_path
185364

186365
# fetch limits from the TLJH config
187366
tljh_config = load_config()
@@ -193,12 +372,47 @@ def tljh_custom_jupyterhub_config(c):
193372
{"default_cpu_limit": cpu_limit, "default_mem_limit": mem_limit}
194373
)
195374

375+
repo_providers = {
376+
'gh': GitHubRepoProvider,
377+
'gist': GistRepoProvider,
378+
'git': GitRepoProvider,
379+
'gl': GitLabRepoProvider,
380+
'zenodo': ZenodoProvider,
381+
'figshare': FigshareProvider,
382+
'hydroshare': HydroshareProvider,
383+
'dataverse': DataverseProvider,
384+
'rdm': RDMProvider,
385+
'weko3': WEKO3Provider,
386+
}
387+
c.RDMProvider.hosts = [
388+
{
389+
'hostname': ["https://osf.io/"],
390+
'api': "https://api.osf.io/v2/"
391+
},
392+
{
393+
'hostname': ["https://bh.rdm.yzwlab.com/"],
394+
'api': "https://api.bh.rdm.yzwlab.com/v2/",
395+
},
396+
{
397+
'hostname': ["https://rcos.rdm.nii.ac.jp"],
398+
'api': "https://api.rcos.rdm.nii.ac.jp/v2/",
399+
},
400+
]
401+
196402
# register the handlers to manage the user images
197403
c.JupyterHub.extra_handlers.extend(
198404
[
199405
(r"environments", ImagesHandler),
200406
(r"api/environments", BuildHandler),
201407
(r"api/environments/([^/]+)/logs", LogsHandler),
408+
(
409+
r"build/([^/]+)/[^/]+/[^/]+",
410+
LaunchHandler,
411+
{
412+
"repo_providers": repo_providers,
413+
"token_store_path": token_store_path,
414+
},
415+
),
202416
(
203417
r"environments-static/(.*)",
204418
CacheControlStaticFilesHandler,
@@ -210,4 +424,9 @@ def tljh_custom_jupyterhub_config(c):
210424

211425
@hookimpl
212426
def tljh_extra_hub_pip_packages():
213-
return ["dockerspawner~=0.11", "jupyter_client~=6.1", "aiodocker~=0.19"]
427+
return [
428+
"dockerspawner~=12.1",
429+
"jupyter_client~=6.1",
430+
"aiodocker~=0.19",
431+
"git+https://github.com/RCOSDP/CS-binderhub.git",
432+
]

tljh_repo2docker/docker.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ async def list_containers():
5656

5757
async def build_image(
5858
repo, ref, name="", memory=None, cpu=None, username=None, password=None,
59-
extra_buildargs=None
59+
extra_buildargs=None, repo2docker_image=None, optional_envs=None, default_image_name=None,
60+
optional_labels=None
6061
):
6162
"""
6263
Build an image given a repo, ref and limits
@@ -67,9 +68,12 @@ async def build_image(
6768

6869
# default to the repo name if no name specified
6970
# and sanitize the name of the docker image
70-
name = name or urlparse(repo).path.strip("/")
71-
name = name.lower().replace("/", "-")
72-
image_name = f"{name}:{ref}"
71+
if default_image_name is not None:
72+
image_name = name = default_image_name
73+
else:
74+
name = name or urlparse(repo).path.strip("/")
75+
name = name.lower().replace("/", "-")
76+
image_name = f"{name}:{ref}"
7377

7478
# memory is specified in GB
7579
memory = f"{memory}G" if memory else ""
@@ -82,6 +86,9 @@ async def build_image(
8286
f"tljh_repo2docker.mem_limit={memory}",
8387
f"tljh_repo2docker.cpu_limit={cpu}",
8488
]
89+
if optional_labels is not None:
90+
labels += [f"tljh_repo2docker.opt.{k}={v}" for k, v in optional_labels.items()]
91+
8592
cmd = [
8693
"jupyter-repo2docker",
8794
"--ref",
@@ -108,10 +115,14 @@ async def build_image(
108115
]
109116

110117
cmd.append(repo)
118+
envs = []
119+
if optional_envs is not None:
120+
for k, v in optional_envs.items():
121+
envs.append(f'{k}={v}')
111122

112123
config = {
113124
"Cmd": cmd,
114-
"Image": "quay.io/jupyterhub/repo2docker:main",
125+
"Image": repo2docker_image or "quay.io/jupyterhub/repo2docker:main",
115126
"Labels": {
116127
"repo2docker.repo": repo,
117128
"repo2docker.ref": ref,
@@ -126,6 +137,7 @@ async def build_image(
126137
"mode": "rw",
127138
}
128139
},
140+
"Env": envs,
129141
"HostConfig": {
130142
"Binds": ["/var/run/docker.sock:/var/run/docker.sock"],
131143
},

0 commit comments

Comments
 (0)