Skip to content

Commit

Permalink
feat(gitlab): add support for paginated project list (#681)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The response of `gitlab_projects` API endpoint has been
modified to also include the pagination details.

Closes #518
  • Loading branch information
mdonadoni committed Mar 20, 2024
1 parent c5f79ee commit b88f105
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 29 deletions.
61 changes: 60 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,73 @@
"name": "search",
"required": false,
"type": "string"
},
{
"description": "Results page number (pagination).",
"in": "query",
"name": "page",
"required": false,
"type": "integer"
},
{
"description": "Number of results per page (pagination).",
"in": "query",
"name": "size",
"required": false,
"type": "integer"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "This resource return all projects owned by the user on GitLab in JSON format."
"description": "This resource return all projects owned by the user on GitLab in JSON format.",
"schema": {
"properties": {
"has_next": {
"type": "boolean"
},
"has_prev": {
"type": "boolean"
},
"items": {
"items": {
"properties": {
"hook_id": {
"type": "integer",
"x-nullable": true
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"page": {
"type": "integer"
},
"size": {
"type": "integer"
},
"total": {
"type": "integer",
"x-nullable": true
}
},
"type": "object"
}
},
"403": {
"description": "Request failed. User token not valid.",
Expand Down
98 changes: 82 additions & 16 deletions reana_server/rest/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from itsdangerous import BadData, TimedJSONWebSignatureSerializer
from reana_commons.k8s.secrets import REANAUserSecretsStore
from werkzeug.local import LocalProxy
from webargs import fields
from webargs import fields, validate
from webargs.flaskparser import use_kwargs


Expand Down Expand Up @@ -187,9 +187,17 @@ def gitlab_oauth(user): # noqa


@blueprint.route("/gitlab/projects", methods=["GET"])
@use_kwargs({"search": fields.Str(location="query")})
@use_kwargs(
{
"search": fields.Str(location="query"),
"page": fields.Int(validate=validate.Range(min=1), location="query"),
"size": fields.Int(validate=validate.Range(min=1), location="query"),
}
)
@signin_required()
def gitlab_projects(user, search: Optional[str] = None): # noqa
def gitlab_projects(
user, search: Optional[str] = None, page: int = 1, size: Optional[int] = None
): # noqa
r"""Endpoint to retrieve GitLab projects.
---
get:
Expand All @@ -210,11 +218,51 @@ def gitlab_projects(user, search: Optional[str] = None): # noqa
description: The search string to filter the project list.
required: false
type: string
- name: page
in: query
description: Results page number (pagination).
required: false
type: integer
- name: size
in: query
description: Number of results per page (pagination).
required: false
type: integer
responses:
200:
description: >-
This resource return all projects owned by
the user on GitLab in JSON format.
schema:
type: object
properties:
has_next:
type: boolean
has_prev:
type: boolean
page:
type: integer
size:
type: integer
total:
type: integer
x-nullable: true
items:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
path:
type: string
url:
type: string
hook_id:
type: integer
x-nullable: true
403:
description: >-
Request failed. User token not valid.
Expand Down Expand Up @@ -252,29 +300,47 @@ def gitlab_projects(user, search: Optional[str] = None): # noqa
# show projects in which user is at least a `Maintainer`
# as that's the minimum access level needed to create webhooks
"min_access_level": 40,
"per_page": 100,
"page": page,
"per_page": size,
"search": search,
# include ancestor namespaces when matching search criteria
"search_namespaces": "true",
# return only basic information about the projects
"simple": "true",
}

response = requests.get(gitlab_url, params=params)
projects = dict()
if response.status_code == 200:
for gitlab_project in response.json():
gitlab_res = requests.get(gitlab_url, params=params)
if gitlab_res.status_code == 200:
projects = list()
for gitlab_project in gitlab_res.json():

Check warning on line 315 in reana_server/rest/gitlab.py

View check run for this annotation

Codecov / codecov/patch

reana_server/rest/gitlab.py#L312-L315

Added lines #L312 - L315 were not covered by tests
hook_id = _get_gitlab_hook_id(gitlab_project["id"], gitlab_token)
projects[gitlab_project["id"]] = {
"name": gitlab_project["name"],
"path": gitlab_project["path_with_namespace"],
"url": gitlab_project["web_url"],
"hook_id": hook_id,
}
return jsonify(projects), 200
projects.append(

Check warning on line 317 in reana_server/rest/gitlab.py

View check run for this annotation

Codecov / codecov/patch

reana_server/rest/gitlab.py#L317

Added line #L317 was not covered by tests
{
"id": gitlab_project["id"],
"name": gitlab_project["name"],
"path": gitlab_project["path_with_namespace"],
"url": gitlab_project["web_url"],
"hook_id": hook_id,
}
)

response = {

Check warning on line 327 in reana_server/rest/gitlab.py

View check run for this annotation

Codecov / codecov/patch

reana_server/rest/gitlab.py#L327

Added line #L327 was not covered by tests
"has_next": bool(gitlab_res.headers.get("x-next-page")),
"has_prev": bool(gitlab_res.headers.get("x-prev-page")),
"items": projects,
"page": int(gitlab_res.headers.get("x-page")),
"size": int(gitlab_res.headers.get("x-per-page")),
"total": (
int(gitlab_res.headers.get("x-total"))
if gitlab_res.headers.get("x-total")
else None
),
}

return jsonify(response), 200

Check warning on line 340 in reana_server/rest/gitlab.py

View check run for this annotation

Codecov / codecov/patch

reana_server/rest/gitlab.py#L340

Added line #L340 was not covered by tests
return (
jsonify({"message": "Project list could not be retrieved"}),
response.status_code,
gitlab_res.status_code,
)
except ValueError:
return jsonify({"message": "Token is not valid."}), 403
Expand Down
31 changes: 19 additions & 12 deletions reana_server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,23 +509,30 @@ def _get_gitlab_hook_id(project_id, gitlab_token):
:param project_id: Project id on GitLab.
:param gitlab_token: GitLab token.
"""
reana_hook_id = None
# FIXME: handle pagination of results
gitlab_hooks_url = (
REANA_GITLAB_URL
+ "/api/v4/projects/{0}/hooks?access_token={1}".format(project_id, gitlab_token)
)
response_json = requests.get(gitlab_hooks_url).json()
create_workflow_url = url_for("workflows.create_workflow", _external=True)
if response_json:
reana_hook_id = next(
(
hook["id"]
for hook in response_json
if hook["url"] and hook["url"] == create_workflow_url
),
None,
response = requests.get(gitlab_hooks_url)

Check warning on line 517 in reana_server/utils.py

View check run for this annotation

Codecov / codecov/patch

reana_server/utils.py#L517

Added line #L517 was not covered by tests

if not response.ok:
logging.warning(

Check warning on line 520 in reana_server/utils.py

View check run for this annotation

Codecov / codecov/patch

reana_server/utils.py#L519-L520

Added lines #L519 - L520 were not covered by tests
f"GitLab hook request failed with status code: {response.status_code}, "
f"content: {response.content}"
)
return reana_hook_id
return None

Check warning on line 524 in reana_server/utils.py

View check run for this annotation

Codecov / codecov/patch

reana_server/utils.py#L524

Added line #L524 was not covered by tests

response_json = response.json()
create_workflow_url = url_for("workflows.create_workflow", _external=True)
return next(

Check warning on line 528 in reana_server/utils.py

View check run for this annotation

Codecov / codecov/patch

reana_server/utils.py#L526-L528

Added lines #L526 - L528 were not covered by tests
(
hook["id"]
for hook in response_json
if hook["url"] and hook["url"] == create_workflow_url
),
None,
)


class RequestStreamWithLen(object):
Expand Down

0 comments on commit b88f105

Please sign in to comment.