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 16, 2021
1 parent 9ae42e7 commit b2050b9
Show file tree
Hide file tree
Showing 21 changed files with 417 additions and 125 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
38 changes: 3 additions & 35 deletions renku/service/controllers/cache_migrations_check.py
Expand Up @@ -17,7 +17,7 @@
# limitations under the License.
"""Renku service migrations check controller."""

from renku.core.commands.migrate import migrations_check, migrations_versions
from renku.core.commands.migrate import migrations_check
from renku.service.controllers.api.abstract import ServiceCtrl
from renku.service.controllers.api.mixins import RenkuOperationMixin
from renku.service.serializers.cache import ProjectMigrationCheckRequest, ProjectMigrationCheckResponseRPC
Expand All @@ -42,40 +42,8 @@ def context(self):

def renku_op(self):
"""Renku operation for the controller."""
latest_version, project_version = migrations_versions().build().execute().output

# TODO: This API design should be improved. Next 2 lines could probably be combined to 1, ie.
# return migrations_check().build().execute().output
(
migration_required,
project_supported,
template_update_possible,
current_template_version,
latest_template_version,
template_source,
template_ref,
template_id,
automated_update,
docker_update_possible,
) = (
migrations_check().build().execute().output
)

return {
"migration_required": migration_required,
"template_update_possible": template_update_possible,
"current_template_version": current_template_version,
"latest_template_version": latest_template_version,
"template_source": template_source,
"template_ref": template_ref,
"template_id": template_id,
"automated_template_update": automated_update,
"docker_update_possible": docker_update_possible,
"project_supported": project_supported,
"project_version": project_version,
"latest_version": latest_version,
}
return migrations_check().build().execute().output

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,
},
)
81 changes: 69 additions & 12 deletions renku/service/serializers/cache.py
Expand Up @@ -251,21 +251,78 @@ class ProjectMigrationCheckRequest(Schema):
branch = fields.String()


class ProjectCompatibilityResponse(Schema):
"""Response schema outlining service compatibility for migrations check."""

project_metadata_version = fields.String(description="Current version of the Renku metadata in the project.")
current_metadata_version = fields.String(description="Highest metadata version supported by this service.")
migration_required = fields.Boolean(
description="Whether or not a metadata migration is required to be compatible with this service."
)


class DockerfileStatusResponse(Schema):
"""Response schema outlining dockerfile status for migrations check."""

newer_renku_available = fields.Boolean(
description="Whether the version of Renku in this service is newer than the one in the Dockerfile."
)
automated_dockerfile_update = fields.Boolean(
description="Whether or not the Dockerfile supports automated Renku version updates."
)
latest_renku_version = fields.String(description="The current version of Renku available in this service.")
dockerfile_renku_version = fields.String(description="Version of Renku specified in the Dockerfile.")


class TemplateStatusResponse(Schema):
"""Response schema outlining template status for migrations check."""

automated_template_update = fields.Boolean(
description="Whether or not the project template explicitly supports automated updates."
)
newer_template_available = fields.Boolean(
description=(
"Whether or not the current version of the project template differs from the " "one used in the project."
)
)
template_source = fields.String(
description="Source of the template repository, either a Git URL or 'renku' if an embedded template was used."
)
template_ref = fields.String(
description="The branch/tag/commit from the template_source repository that was used to create this project."
)
template_id = fields.String(description="The id of the template in the template repository.")

project_template_version = fields.String(
allow_none=True, description="The version of the template last used in the user's project."
)
latest_template_version = fields.String(
allow_none=True, description="The current version of the template in the template repository."
)


class ProjectMigrationCheckResponse(Schema):
"""Response schema for project migration check."""

migration_required = fields.Boolean()
template_update_possible = fields.Boolean()
automated_template_update = fields.Boolean()
current_template_version = fields.String(allow_none=True)
latest_template_version = fields.String(allow_none=True)
template_source = fields.String()
template_ref = fields.String()
template_id = fields.String()
docker_update_possible = fields.Boolean()
project_supported = fields.Boolean()
project_version = fields.String()
latest_version = fields.String()
project_supported = fields.Boolean(
description=(
"Determines whether this project is a Renku project that is supported by the version "
"running on this service (not made with a newer version)."
)
)
core_renku_version = fields.String(description="Version of Renku running in this service.")
project_renku_version = fields.String(description="Version of Renku last used to change the project.")

core_compatibility_status = fields.Nested(
ProjectCompatibilityResponse,
description="Fields detailing the compatibility of the project with this core service version.",
)
dockerfile_renku_status = fields.Nested(
DockerfileStatusResponse, description="Fields detailing the status of the Dockerfile in the project."
)
template_status = fields.Nested(
TemplateStatusResponse, description="Fields detailing the status of the project template used by this project."
)


class ProjectMigrationCheckResponseRPC(JsonRPCResponse):
Expand Down
18 changes: 18 additions & 0 deletions renku/service/serializers/v0_9/__init__.py
@@ -0,0 +1,18 @@
# -*- 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 v0.9 serializers."""
46 changes: 46 additions & 0 deletions renku/service/serializers/v0_9/cache.py
@@ -0,0 +1,46 @@
# -*- 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 cache serializers."""

from marshmallow import Schema, fields

from renku.service.serializers.rpc import JsonRPCResponse


class ProjectMigrationCheckResponse_0_9(Schema):
"""Response schema for project migration check."""

migration_required = fields.Boolean(attribute="core_compatibility_status.migration_required")
template_update_possible = fields.Boolean(attribute="template_status.newer_template_available")
automated_template_update = fields.Boolean(attribute="template_status.automated_template_update")
current_template_version = fields.String(attribute="template_status.project_template_version")
latest_template_version = fields.String(attribute="template_status.latest_template_version")
template_source = fields.String(attribute="template_status.template_source")
template_ref = fields.String(attribute="template_status.template_ref")
template_id = fields.String(attribute="template_status.template_id")
docker_update_possible = fields.Boolean(attribute="dockerfile_renku_status.newer_renku_available")

project_supported = fields.Boolean()
project_renku_version = fields.String(data_key="project_version")
core_renku_version = fields.String(data_key="latest_version")


class ProjectMigrationCheckResponseRPC_0_9(JsonRPCResponse):
"""RPC response schema for project migration check."""

result = fields.Nested(ProjectMigrationCheckResponse_0_9)
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

0 comments on commit b2050b9

Please sign in to comment.