Skip to content

Commit 3c2bc8d

Browse files
committed
feat: added renku service with cache and datasets
1 parent 6084c87 commit 3c2bc8d

22 files changed

+1930
-83
lines changed

Dockerfile renamed to Dockerfile.cli

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.6-alpine as base
1+
FROM python:3.7-alpine as base
22

33
RUN apk add --no-cache git && \
44
pip install --no-cache --upgrade pip

Dockerfile.svc

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM python:3.7-alpine
2+
3+
RUN apk add --update --no-cache alpine-sdk g++ gcc linux-headers libxslt-dev python3-dev build-base openssl-dev libffi-dev git && \
4+
pip install --no-cache --upgrade pip setuptools pipenv requirements-builder
5+
6+
RUN apk add --no-cache --allow-untrusted \
7+
--repository http://dl-cdn.alpinelinux.org/alpine/latest-stable/community \
8+
--repository http://dl-cdn.alpinelinux.org/alpine/latest-stable/main \
9+
--repository http://nl.alpinelinux.org/alpine/edge/community \
10+
git-lfs && \
11+
git lfs install
12+
13+
COPY . /code/renku
14+
WORKDIR /code/renku
15+
RUN requirements-builder -e all --level=pypi setup.py > requirements.txt && pip install -r requirements.txt && pip install -e . && pip install gunicorn
16+
17+
18+
ENTRYPOINT ["gunicorn", "renku.service.entrypoint:app", "-b", "0.0.0.0:8080"]

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ include babel.ini
3939
include brew.py
4040
include pytest.ini
4141
include snap/snapcraft.yaml
42+
recursive-include renku *.json
4243
recursive-include .github CODEOWNERS
4344
recursive-include .travis *.sh
4445
recursive-include docs *.bat

Pipfile.lock

+124-74
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conftest.py

+39
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import responses
3131
from click.testing import CliRunner
3232

33+
from renku.service.entrypoint import create_app
34+
3335

3436
@pytest.fixture(scope='module')
3537
def renku_path(tmpdir_factory):
@@ -474,3 +476,40 @@ def sleep_after():
474476
import time
475477
yield
476478
time.sleep(0.5)
479+
480+
481+
@pytest.fixture(scope='module')
482+
def svc_client():
483+
"""Renku service client."""
484+
flask_app = create_app()
485+
486+
testing_client = flask_app.test_client()
487+
testing_client.testing = True
488+
489+
ctx = flask_app.app_context()
490+
ctx.push()
491+
492+
yield testing_client
493+
494+
ctx.pop()
495+
496+
497+
@pytest.fixture(scope='function')
498+
def svc_client_with_repo(svc_client):
499+
"""Renku service remote repository."""
500+
access_token = 'contact:EcfPJvEqjJepyu6XyqKZ'
501+
remote_url = 'https://{0}@renkulab.io/gitlab/contact/integration-tests.git'
502+
headers = {'Authorization': 'Bearer b4b4de0eda0f471ab82702bd5c367fa7'}
503+
504+
params = {'git_url': remote_url.format(access_token), 'force': 1}
505+
506+
response = svc_client.get(
507+
'/cache/project-clone', query_string=params, headers=headers
508+
)
509+
510+
assert response
511+
assert 'result' in response.json
512+
assert 'error' not in response.json
513+
assert 'integration-tests' == response.json['result']['project_id']
514+
515+
yield svc_client, headers

renku/core/commands/client.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import yaml
2626

2727
from renku.core.management import LocalClient
28+
from renku.core.management.config import RENKU_HOME
29+
from renku.core.management.repository import default_path
2830

2931
from .git import get_git_isolation
3032

@@ -63,8 +65,17 @@ def pass_local_client(
6365
)
6466

6567
def new_func(*args, **kwargs):
66-
ctx = click.get_current_context()
67-
client = ctx.ensure_object(LocalClient)
68+
ctx = click.get_current_context(silent=True)
69+
if not ctx:
70+
client = LocalClient(
71+
path=default_path(),
72+
renku_home=RENKU_HOME,
73+
use_external_storage=True,
74+
)
75+
ctx = click.Context(click.Command(method))
76+
else:
77+
client = ctx.ensure_object(LocalClient)
78+
6879
stack = contextlib.ExitStack()
6980

7081
# Handle --isolation option:
@@ -85,8 +96,11 @@ def new_func(*args, **kwargs):
8596
if lock or (lock is None and commit):
8697
stack.enter_context(client.lock)
8798

88-
with stack:
89-
result = ctx.invoke(method, client, *args, **kwargs)
99+
result = None
100+
if ctx:
101+
with stack:
102+
result = ctx.invoke(method, client, *args, **kwargs)
103+
90104
return result
91105

92106
return functools.update_wrapper(new_func, method)

renku/core/commands/dataset.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def create_dataset(client, name, handle_duplicate_fn=None):
106106
:raises: ``renku.core.errors.ParameterError``
107107
"""
108108
existing = client.load_dataset(name=name)
109-
if (not existing or handle_duplicate_fn and handle_duplicate_fn(existing)):
109+
if not existing or handle_duplicate_fn and handle_duplicate_fn(existing):
110110
with client.with_dataset(name=name) as dataset:
111111
creator = Creator.from_git(client.repo)
112112
if creator not in dataset.creator:
@@ -143,9 +143,12 @@ def add_file(
143143
sources=(),
144144
destination='',
145145
with_metadata=None,
146-
urlscontext=contextlib.nullcontext
146+
urlscontext=contextlib.nullcontext,
147+
use_external_storage=True
147148
):
148149
"""Add data file to a dataset."""
150+
client.use_external_storage = use_external_storage
151+
149152
add_to_dataset(
150153
client, urls, name, link, force, sources, destination, with_metadata,
151154
urlscontext

renku/core/management/datasets.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,8 @@ def _add_from_url(self, dataset, dataset_path, url, link, destination):
296296
mode = dst.stat().st_mode & 0o777
297297
dst.chmod(mode & ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
298298

299-
self.track_paths_in_storage(str(dst.relative_to(self.path)))
299+
if self.has_external_storage:
300+
self.track_paths_in_storage(str(dst.relative_to(self.path)))
300301

301302
return [{
302303
'path': dst.relative_to(self.path),

renku/core/management/repository.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,18 @@ def default_path():
4747
return '.'
4848

4949

50+
def path_converter(path):
51+
"""Converter for path in PathMixin."""
52+
return Path(path).resolve()
53+
54+
5055
@attr.s
5156
class PathMixin:
5257
"""Define a default path attribute."""
5358

5459
path = attr.ib(
5560
default=default_path,
56-
converter=lambda arg: Path(arg).resolve().absolute(),
61+
converter=path_converter,
5762
)
5863

5964
@path.validator

renku/service/Dockerfile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.6
2+
ADD . /app
3+
WORKDIR /app
4+
RUN pip install -e .
5+
EXPOSE 8000
6+
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app"]

renku/service/__init__.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2019 - Swiss Data Science Center (SDSC)
4+
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
5+
# Eidgenössische Technische Hochschule Zürich (ETHZ).
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
"""Renku service."""

renku/service/config.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2019 - Swiss Data Science Center (SDSC)
4+
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
5+
# Eidgenössische Technische Hochschule Zürich (ETHZ).
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
"""Renku service config."""
19+
import os
20+
import tempfile
21+
from pathlib import Path
22+
23+
INVALID_PARAMS_ERROR_CODE = -32602
24+
INTERNAL_FAILURE_ERROR_CODE = -32603
25+
26+
API_VERSION = 'v1'
27+
28+
SWAGGER_URL = '/api/docs'
29+
API_URL = os.getenv(
30+
'RENKU_SVC_SWAGGER_URL', '/api/{0}/spec'.format(API_VERSION)
31+
)
32+
33+
UPLOAD_FOLDER = tempfile.TemporaryDirectory()
34+
35+
CACHE_UPLOADS_PATH = Path(UPLOAD_FOLDER.name) / Path('uploads')
36+
CACHE_PROJECTS_PATH = Path(UPLOAD_FOLDER.name) / Path('projects')
37+
38+
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'csv'}
39+
40+
JWT_ALGORITHM = 'HS256'
41+
JWT_KEY = 'renku-svc-secret-key'

renku/service/entrypoint.py

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2019 - Swiss Data Science Center (SDSC)
4+
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
5+
# Eidgenössische Technische Hochschule Zürich (ETHZ).
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
"""Renku service entry point."""
19+
import os
20+
import uuid
21+
22+
from flask import Flask
23+
from flask_swagger_ui import get_swaggerui_blueprint
24+
25+
from renku.service.config import API_URL, API_VERSION, CACHE_PROJECTS_PATH, \
26+
CACHE_UPLOADS_PATH, SWAGGER_URL, UPLOAD_FOLDER
27+
from renku.service.views.cache import clone_repository_view, \
28+
list_projects_view, list_uploaded_files_view, upload_files_view
29+
from renku.service.views.datasets import add_file_to_dataset_view, \
30+
create_dataset_view, list_dataset_files_view, list_datasets_view
31+
from renku.service.views.docs import api_docs_view
32+
33+
34+
def make_cache():
35+
"""Create cache structure."""
36+
sub_dirs = [CACHE_UPLOADS_PATH, CACHE_PROJECTS_PATH]
37+
38+
for subdir in sub_dirs:
39+
if not subdir.exists():
40+
subdir.mkdir()
41+
42+
43+
def create_app():
44+
"""Creates a Flask app with necessary configuration."""
45+
app = Flask(__name__)
46+
app.secret_key = os.getenv('RENKU_SVC_SERVICE_KEY', uuid.uuid4().hex)
47+
48+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER.name
49+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
50+
51+
make_cache()
52+
build_routes(app)
53+
54+
return app
55+
56+
57+
def build_routes(app):
58+
"""Register routes to given app instance."""
59+
swaggerui_blueprint = get_swaggerui_blueprint(
60+
SWAGGER_URL, API_URL, config={'app_name': 'RenkuSvc'}
61+
)
62+
63+
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
64+
65+
app.add_url_rule(
66+
'/api/{0}/spec'.format(API_VERSION),
67+
'docs',
68+
api_docs_view,
69+
methods=['GET']
70+
)
71+
72+
app.add_url_rule(
73+
'/cache/files-list',
74+
'list_files',
75+
list_uploaded_files_view,
76+
methods=['GET']
77+
)
78+
79+
app.add_url_rule(
80+
'/cache/files-upload',
81+
'upload_files',
82+
upload_files_view,
83+
methods=['POST']
84+
)
85+
86+
app.add_url_rule(
87+
'/cache/project-clone',
88+
'clone_project',
89+
clone_repository_view,
90+
methods=['GET']
91+
)
92+
93+
app.add_url_rule(
94+
'/cache/project-list',
95+
'list_projects',
96+
list_projects_view,
97+
methods=['GET']
98+
)
99+
100+
app.add_url_rule(
101+
'/datasets/list', 'list_datasets', list_datasets_view, methods=['GET']
102+
)
103+
104+
app.add_url_rule(
105+
'/datasets/files',
106+
'list_dataset_files',
107+
list_dataset_files_view,
108+
methods=['GET']
109+
)
110+
111+
app.add_url_rule(
112+
'/datasets/add',
113+
'add_dataset_file',
114+
add_file_to_dataset_view,
115+
methods=['GET']
116+
)
117+
118+
app.add_url_rule(
119+
'/datasets/create',
120+
'create_data',
121+
create_dataset_view,
122+
methods=['GET']
123+
)
124+
125+
126+
app = create_app()
127+
128+
if __name__ == '__main__':
129+
app.run()

renku/service/management/__init__.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2019 - Swiss Data Science Center (SDSC)
4+
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
5+
# Eidgenössische Technische Hochschule Zürich (ETHZ).
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
"""Renku service management commands."""

0 commit comments

Comments
 (0)