Skip to content

Commit 3f716f7

Browse files
committed
fix(gitlab): handle pagination of GitLab webhooks (#684)
Closes #682
1 parent 4d23c62 commit 3f716f7

File tree

4 files changed

+60
-20
lines changed

4 files changed

+60
-20
lines changed

reana_server/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,8 @@ def _get_rate_limit(env_variable: str, default: str) -> str:
328328
REANA_GITLAB_OAUTH_APP_SECRET = os.getenv("REANA_GITLAB_OAUTH_APP_SECRET", "CHANGE_ME")
329329
REANA_GITLAB_HOST = os.getenv("REANA_GITLAB_HOST", None)
330330
REANA_GITLAB_URL = "https://{}".format((REANA_GITLAB_HOST or "CHANGE ME"))
331+
REANA_GITLAB_MAX_PER_PAGE = 100
332+
"""Maximum number of items that can be listed in a single GitLab's paginated response."""
331333

332334
# Workflow scheduler
333335
# ==================

reana_server/utils.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import secrets
1818
import sys
1919
import shutil
20-
from typing import Dict, List, Optional, Union
20+
from typing import Any, Dict, List, Optional, Union, Generator
2121
from uuid import UUID, uuid4
2222

2323
import click
@@ -67,6 +67,7 @@
6767
)
6868
from reana_server.config import (
6969
ADMIN_USER_ID,
70+
REANA_GITLAB_MAX_PER_PAGE,
7071
REANA_GITLAB_URL,
7172
REANA_HOSTNAME,
7273
REANA_USER_EMAIL_CONFIRMATION,
@@ -500,6 +501,19 @@ def _format_gitlab_secrets(gitlab_response):
500501
}
501502

502503

504+
def _unpaginate_gitlab_endpoint(url: str) -> Generator[Any, None, None]:
505+
"""Get all the paginated records of a given GitLab endpoint.
506+
507+
:param url: Endpoint URL to the first page.
508+
"""
509+
while url:
510+
logging.debug(f"Request to '{url}' while unpaginating GitLab endpoint")
511+
response = requests.get(url)
512+
response.raise_for_status()
513+
yield from response.json()
514+
url = response.links.get("next", {}).get("url")
515+
516+
503517
def _get_gitlab_hook_id(project_id, gitlab_token):
504518
"""Return REANA hook id from a GitLab project if it is connected.
505519
@@ -511,27 +525,22 @@ def _get_gitlab_hook_id(project_id, gitlab_token):
511525
"""
512526
gitlab_hooks_url = (
513527
REANA_GITLAB_URL
514-
+ "/api/v4/projects/{0}/hooks?access_token={1}".format(project_id, gitlab_token)
528+
+ f"/api/v4/projects/{project_id}/hooks?"
529+
+ f"per_page={REANA_GITLAB_MAX_PER_PAGE}&"
530+
+ f"access_token={gitlab_token}"
515531
)
516-
response = requests.get(gitlab_hooks_url)
532+
create_workflow_url = url_for("workflows.create_workflow", _external=True)
517533

518-
if not response.ok:
534+
try:
535+
for hook in _unpaginate_gitlab_endpoint(gitlab_hooks_url):
536+
if hook["url"] and hook["url"] == create_workflow_url:
537+
return hook["id"]
538+
except requests.HTTPError as e:
519539
logging.warning(
520-
f"GitLab hook request failed with status code: {response.status_code}, "
521-
f"content: {response.content}"
540+
f"GitLab hook request failed with status code: {e.response.status_code}, "
541+
f"content: {e.response.content}"
522542
)
523-
return None
524-
525-
response_json = response.json()
526-
create_workflow_url = url_for("workflows.create_workflow", _external=True)
527-
return next(
528-
(
529-
hook["id"]
530-
for hook in response_json
531-
if hook["url"] and hook["url"] == create_workflow_url
532-
),
533-
None,
534-
)
543+
return None
535544

536545

537546
class RequestStreamWithLen(object):

tests/test_utils.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
# This file is part of REANA.
2-
# Copyright (C) 2021, 2022, 2023 CERN.
2+
# Copyright (C) 2021, 2022, 2023, 2024 CERN.
33
#
44
# REANA is free software; you can redistribute it and/or modify it
55
# under the terms of the MIT License; see LICENSE file for more details.
66

77
"""REANA-Server tests for utils module."""
88

99
import pathlib
10+
from unittest.mock import call, patch, Mock
1011
import pytest
1112

1213
from reana_commons.errors import REANAValidationError
1314
from reana_db.models import UserToken, UserTokenStatus, UserTokenType
14-
from reana_server.utils import is_valid_email, filter_input_files, get_user_from_token
15+
from reana_server.utils import (
16+
is_valid_email,
17+
filter_input_files,
18+
get_user_from_token,
19+
_unpaginate_gitlab_endpoint,
20+
)
1521

1622

1723
@pytest.mark.parametrize(
@@ -81,3 +87,25 @@ def test_get_user_from_token_two_tokens(default_user, session):
8187
# Check that old revoked token does not work
8288
with pytest.raises(ValueError, match="revoked"):
8389
get_user_from_token(old_token.token)
90+
91+
92+
@patch("requests.get")
93+
def test_gitlab_pagination(mock_get):
94+
"""Test getting all paginated results from GitLab."""
95+
# simulating two pages
96+
first_response = Mock()
97+
first_response.ok = True
98+
first_response.links = {"next": {"url": "next_url"}}
99+
first_response.json.return_value = [1, 2]
100+
101+
second_response = Mock()
102+
second_response.ok = True
103+
second_response.links = {}
104+
second_response.json.return_value = [3, 4]
105+
106+
mock_get.side_effect = [first_response, second_response]
107+
108+
res = list(_unpaginate_gitlab_endpoint("first_url"))
109+
110+
assert res == [1, 2, 3, 4]
111+
assert mock_get.call_args_list == [call("first_url"), call("next_url")]

tests/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,7 @@ def test_gitlab_projects(app: Flask, default_user):
835835
mock_response_webhook = Mock()
836836
mock_response_webhook.ok = True
837837
mock_response_webhook.status_code = 200
838+
mock_response_webhook.links = {}
838839
mock_response_webhook.json.return_value = [
839840
{"id": 1234, "url": "wrong_url"},
840841
{

0 commit comments

Comments
 (0)