Skip to content

Commit

Permalink
feat(service): backport API versioning changes from 1.0.0 release
Browse files Browse the repository at this point in the history
Conflicts:
  • Loading branch information
Panaetius committed Nov 17, 2021
1 parent 9ae42e7 commit 772d8bc
Show file tree
Hide file tree
Showing 17 changed files with 208 additions and 79 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/acceptance-tests.yml
Expand Up @@ -27,6 +27,7 @@ jobs:
renku-notebooks: ${{ steps.deploy-comment.outputs.renku-notebooks}}
renku-ui: ${{ steps.deploy-comment.outputs.renku-ui}}
test-enabled: ${{ steps.deploy-comment.outputs.test-enabled}}
extra-values: ${{ steps.deploy-comment.outputs.extra-values}}
steps:
- id: deploy-comment
uses: SwissDataScienceCenter/renku-actions/check-pr-description@v0.1.0
Expand Down Expand Up @@ -63,6 +64,7 @@ jobs:
renku_graph: "${{ needs.check-deploy.outputs.renku-graph }}"
renku_notebooks: "${{ needs.check-deploy.outputs.renku-notebooks }}"
renku_ui: "${{ needs.check-deploy.outputs.renku-ui }}"
extra_values: "${{ needs.check-deploy.outputs.extra-values }}"
- name: Check existing renkubot comment
uses: peter-evans/find-comment@v1
id: findcomment
Expand Down
2 changes: 1 addition & 1 deletion renku/service/controllers/cache_migrations_check.py
Expand Up @@ -78,4 +78,4 @@ def renku_op(self):

def to_response(self):
"""Execute controller flow and serialize to service response."""
return result_response(MigrationsCheckCtrl.RESPONSE_SERIALIZER, self.execute_op())
return result_response(self.RESPONSE_SERIALIZER, self.execute_op())
9 changes: 7 additions & 2 deletions renku/service/controllers/version.py
Expand Up @@ -28,9 +28,14 @@ class VersionCtrl(ServiceCtrl):

RESPONSE_SERIALIZER = VersionResponseRPC()

def to_response(self):
def to_response(self, minimum_version, maximum_version):
"""Serialize to service version response."""
return result_response(
VersionCtrl.RESPONSE_SERIALIZER,
{"latest_version": __version__, "supported_project_version": SUPPORTED_PROJECT_VERSION},
{
"latest_version": __version__,
"supported_project_version": SUPPORTED_PROJECT_VERSION,
"minimum_api_version": minimum_version.name,
"maximum_api_version": maximum_version.name,
},
)
2 changes: 2 additions & 0 deletions renku/service/serializers/version.py
Expand Up @@ -22,6 +22,8 @@ class VersionResponse(Schema):

latest_version = fields.String()
supported_project_version = fields.Number()
minimum_api_version = fields.String()
maximum_api_version = fields.String()


class VersionResponseRPC(Schema):
Expand Down
61 changes: 61 additions & 0 deletions renku/service/views/api_versions.py
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# Copyright 2020 - Swiss Data Science Center (SDSC)
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku service api versions."""
from typing import Any, Callable, List, Optional

from flask import Blueprint


class ApiVersion:
"""Represents a Blueprint API version."""

def __init__(self, name: str, is_base_version: bool = False):
self.name = name
self.is_base_version = is_base_version


class VersionedBlueprint(Blueprint):
"""A Blueprint that supports versioning."""

def add_url_rule(
self,
rule: str,
endpoint: Optional[str] = None,
view_func: Optional[Callable] = None,
provide_automatic_options: Optional[bool] = None,
versions: List[str] = None,
**options: Any,
) -> None:
"""Overwrite Blueprint add_url_rule to support versioning."""

for version in versions:
if version.is_base_version:
super().add_url_rule(
rule, endpoint, view_func, provide_automatic_options=provide_automatic_options, **options
)

version_rule = f"/{version.name}{rule}"
super().add_url_rule(
version_rule, endpoint, view_func, provide_automatic_options=provide_automatic_options, **options
)


V0_9 = ApiVersion("0.9", is_base_version=True)

MINIMUM_VERSION = V0_9
MAXIMUM_VERSION = V0_9
24 changes: 21 additions & 3 deletions renku/service/views/apispec.py
Expand Up @@ -16,10 +16,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku service apispec views."""
from apispec import APISpec
from apispec import APISpec, yaml_utils
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flask import Blueprint, current_app, jsonify
from flask.views import MethodView

from renku.service.config import (
API_VERSION,
Expand Down Expand Up @@ -85,11 +86,28 @@
"""


class MultiURLFlaskPlugin(FlaskPlugin):
"""FlaskPlugin extension that supports multiple URLs per endpoint."""

def path_helper(self, path, operations, *, view, app=None, **kwargs):
"""Path helper that allows passing a Flask view function."""
rule = self._rule_for_view(view, app=app)
operations.update(yaml_utils.load_operations_from_docstring(view.__doc__))
if hasattr(view, "view_class") and issubclass(view.view_class, MethodView):
for method in view.methods:
if method in rule.methods:
method_name = method.lower()
method = getattr(view.view_class, method_name)
operations[method_name] = yaml_utils.load_yaml_from_docstring(method.__doc__)
return self.flaskpath2openapi(path)


spec = APISpec(
title=SERVICE_NAME,
openapi_version=OPENAPI_VERSION,
version=API_VERSION,
plugins=[FlaskPlugin(), MarshmallowPlugin()],
plugins=[MultiURLFlaskPlugin(), MarshmallowPlugin()],
servers=[{"url": SERVICE_API_BASE_PATH}],
security=[{"oidc": []}, {"JWT": [], "gitlab-token": []}],
info={"description": TOP_LEVEL_DESCRIPTION},
Expand All @@ -109,5 +127,5 @@ def openapi():
def get_apispec(app):
"""Return the apispec."""
for rule in current_app.url_map.iter_rules():
spec.path(view=app.view_functions[rule.endpoint])
spec.path(path=rule.rule, view=app.view_functions[rule.endpoint])
return spec
17 changes: 9 additions & 8 deletions renku/service/views/cache.py
Expand Up @@ -16,7 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku service cache views."""
from flask import Blueprint, request
from flask import request

from renku.service.config import SERVICE_PREFIX
from renku.service.controllers.cache_files_upload import UploadFilesCtrl
Expand All @@ -25,6 +25,7 @@
from renku.service.controllers.cache_migrate_project import MigrateProjectCtrl
from renku.service.controllers.cache_migrations_check import MigrationsCheckCtrl
from renku.service.controllers.cache_project_clone import ProjectCloneCtrl
from renku.service.views.api_versions import V0_9, VersionedBlueprint
from renku.service.views.decorators import (
accepts_json,
handle_common_except,
Expand All @@ -34,10 +35,10 @@
)

CACHE_BLUEPRINT_TAG = "cache"
cache_blueprint = Blueprint("cache", __name__, url_prefix=SERVICE_PREFIX)
cache_blueprint = VersionedBlueprint("cache", __name__, url_prefix=SERVICE_PREFIX)


@cache_blueprint.route("/cache.files_list", methods=["GET"], provide_automatic_options=False)
@cache_blueprint.route("/cache.files_list", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@requires_identity
Expand All @@ -60,7 +61,7 @@ def list_uploaded_files_view(user_data, cache):
return ListUploadedFilesCtrl(cache, user_data).to_response()


@cache_blueprint.route("/cache.files_upload", methods=["POST"], provide_automatic_options=False)
@cache_blueprint.route("/cache.files_upload", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@requires_identity
Expand Down Expand Up @@ -95,7 +96,7 @@ def upload_file_view(user_data, cache):
return UploadFilesCtrl(cache, user_data, request).to_response()


@cache_blueprint.route("/cache.project_clone", methods=["POST"], provide_automatic_options=False)
@cache_blueprint.route("/cache.project_clone", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -124,7 +125,7 @@ def project_clone_view(user_data, cache):
return ProjectCloneCtrl(cache, user_data, dict(request.json)).to_response()


@cache_blueprint.route("/cache.project_list", methods=["GET"], provide_automatic_options=False)
@cache_blueprint.route("/cache.project_list", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@requires_identity
Expand All @@ -147,7 +148,7 @@ def list_projects_view(user_data, cache):
return ListProjectsCtrl(cache, user_data).to_response()


@cache_blueprint.route("/cache.migrate", methods=["POST"], provide_automatic_options=False)
@cache_blueprint.route("/cache.migrate", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@handle_migration_except
@accepts_json
Expand Down Expand Up @@ -176,7 +177,7 @@ def migrate_project_view(user_data, cache):
return MigrateProjectCtrl(cache, user_data, dict(request.json)).to_response()


@cache_blueprint.route("/cache.migrations_check", methods=["GET"], provide_automatic_options=False)
@cache_blueprint.route("/cache.migrations_check", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@requires_identity
Expand Down
9 changes: 5 additions & 4 deletions renku/service/views/config.py
Expand Up @@ -16,11 +16,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku service project config views."""
from flask import Blueprint, request
from flask import request

from renku.service.config import SERVICE_PREFIX
from renku.service.controllers.config_set import SetConfigCtrl
from renku.service.controllers.config_show import ShowConfigCtrl
from renku.service.views.api_versions import V0_9, VersionedBlueprint
from renku.service.views.decorators import (
accepts_json,
handle_common_except,
Expand All @@ -30,10 +31,10 @@
)

CONFIG_BLUEPRINT_TAG = "config"
config_blueprint = Blueprint("config", __name__, url_prefix=SERVICE_PREFIX)
config_blueprint = VersionedBlueprint("config", __name__, url_prefix=SERVICE_PREFIX)


@config_blueprint.route("/config.show", methods=["GET"], provide_automatic_options=False)
@config_blueprint.route("/config.show", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@optional_identity
Expand All @@ -59,7 +60,7 @@ def show_config(user_data, cache):
return ShowConfigCtrl(cache, user_data, dict(request.args)).to_response()


@config_blueprint.route("/config.set", methods=["POST"], provide_automatic_options=False)
@config_blueprint.route("/config.set", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down
21 changes: 11 additions & 10 deletions renku/service/views/datasets.py
Expand Up @@ -16,7 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku service datasets view."""
from flask import Blueprint, request
from flask import request

from renku.service.config import SERVICE_PREFIX
from renku.service.controllers.datasets_add_file import DatasetsAddFileCtrl
Expand All @@ -27,6 +27,7 @@
from renku.service.controllers.datasets_list import DatasetsListCtrl
from renku.service.controllers.datasets_remove import DatasetsRemoveCtrl
from renku.service.controllers.datasets_unlink import DatasetsUnlinkCtrl
from renku.service.views.api_versions import V0_9, VersionedBlueprint
from renku.service.views.decorators import (
accepts_json,
handle_common_except,
Expand All @@ -36,10 +37,10 @@
)

DATASET_BLUEPRINT_TAG = "datasets"
dataset_blueprint = Blueprint(DATASET_BLUEPRINT_TAG, __name__, url_prefix=SERVICE_PREFIX)
dataset_blueprint = VersionedBlueprint(DATASET_BLUEPRINT_TAG, __name__, url_prefix=SERVICE_PREFIX)


@dataset_blueprint.route("/datasets.list", methods=["GET"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.list", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@optional_identity
Expand All @@ -65,7 +66,7 @@ def list_datasets_view(user_data, cache):
return DatasetsListCtrl(cache, user_data, dict(request.args)).to_response()


@dataset_blueprint.route("/datasets.files_list", methods=["GET"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.files_list", methods=["GET"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@requires_cache
@optional_identity
Expand All @@ -91,7 +92,7 @@ def list_dataset_files_view(user_data, cache):
return DatasetsFilesListCtrl(cache, user_data, dict(request.args)).to_response()


@dataset_blueprint.route("/datasets.add", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.add", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -119,7 +120,7 @@ def add_file_to_dataset_view(user_data, cache):
return DatasetsAddFileCtrl(cache, user_data, dict(request.json)).to_response()


@dataset_blueprint.route("/datasets.create", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.create", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -147,7 +148,7 @@ def create_dataset_view(user_data, cache):
return DatasetsCreateCtrl(cache, user_data, dict(request.json)).to_response()


@dataset_blueprint.route("/datasets.remove", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.remove", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -175,7 +176,7 @@ def remove_dataset_view(user_data, cache):
return DatasetsRemoveCtrl(cache, user_data, dict(request.json)).to_response()


@dataset_blueprint.route("/datasets.import", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.import", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -203,7 +204,7 @@ def import_dataset_view(user_data, cache):
return DatasetsImportCtrl(cache, user_data, dict(request.json)).to_response()


@dataset_blueprint.route("/datasets.edit", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.edit", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down Expand Up @@ -231,7 +232,7 @@ def edit_dataset_view(user_data, cache):
return DatasetsEditCtrl(cache, user_data, dict(request.json)).to_response()


@dataset_blueprint.route("/datasets.unlink", methods=["POST"], provide_automatic_options=False)
@dataset_blueprint.route("/datasets.unlink", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@requires_cache
Expand Down
7 changes: 4 additions & 3 deletions renku/service/views/graph.py
Expand Up @@ -16,17 +16,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku graph endpoints."""
from flask import Blueprint, request
from flask import request

from renku.service.config import SERVICE_PREFIX
from renku.service.controllers.graph_build import GraphBuildCtrl
from renku.service.views.api_versions import V0_9, VersionedBlueprint
from renku.service.views.decorators import accepts_json, handle_common_except, optional_identity

GRAPH_BLUEPRINT_TAG = "graph"
graph_blueprint = Blueprint(GRAPH_BLUEPRINT_TAG, __name__, url_prefix=SERVICE_PREFIX)
graph_blueprint = VersionedBlueprint(GRAPH_BLUEPRINT_TAG, __name__, url_prefix=SERVICE_PREFIX)


@graph_blueprint.route("/graph.build", methods=["POST"], provide_automatic_options=False)
@graph_blueprint.route("/graph.build", methods=["POST"], provide_automatic_options=False, versions=[V0_9])
@handle_common_except
@accepts_json
@optional_identity
Expand Down

0 comments on commit 772d8bc

Please sign in to comment.