From 9afd8ab08d433fe992fdc898c74be60701c6a91f Mon Sep 17 00:00:00 2001 From: George Katsikas <giorgakis.katsikas@gmail.com> Date: Mon, 15 May 2023 16:02:54 +0200 Subject: [PATCH] add basic gitlab integration with main server add python-gitlab package to requirements add gitlab variables to settings add advance repos django command --- requirements.txt | 3 + scipost_django/SciPost_v1/settings/base.py | 5 + .../production/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/advance_git_repos.py | 154 ++++++++++++++++++ scipost_django/production/models.py | 10 +- 6 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 scipost_django/production/management/__init__.py create mode 100644 scipost_django/production/management/commands/__init__.py create mode 100644 scipost_django/production/management/commands/advance_git_repos.py diff --git a/requirements.txt b/requirements.txt index 63519f6e9..a3a6d753e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -86,3 +86,6 @@ mailchimp3==3.0.18 # 2023-05-09 django-referrer-policy==1.0 # 2020-09-19 no new updates for 3 years django-csp==3.7 # 2020-09-19 django-feature-policy==3.4.0 # 2020-09-19 + +# Version Control +python-gitlab==3.14.0 # 2023-05-15 \ No newline at end of file diff --git a/scipost_django/SciPost_v1/settings/base.py b/scipost_django/SciPost_v1/settings/base.py index 9d3501b30..f00e676ff 100644 --- a/scipost_django/SciPost_v1/settings/base.py +++ b/scipost_django/SciPost_v1/settings/base.py @@ -570,3 +570,8 @@ DISCOURSE_SSO_SECRET = get_secret("DISCOURSE_SSO_SECRET") CORS_ALLOWED_ORIGINS = [ "https://git.scipost.org", ] + +# GitLab API +GITLAB_ROOT = "SciPost" +GITLAB_URL = "https://git.scipost.org/" +GITLAB_KEY = get_secret("GITLAB_KEY") diff --git a/scipost_django/production/management/__init__.py b/scipost_django/production/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scipost_django/production/management/commands/__init__.py b/scipost_django/production/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scipost_django/production/management/commands/advance_git_repos.py b/scipost_django/production/management/commands/advance_git_repos.py new file mode 100644 index 000000000..af977dd98 --- /dev/null +++ b/scipost_django/production/management/commands/advance_git_repos.py @@ -0,0 +1,154 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + +from django.core.management.base import BaseCommand, CommandParser +from django.conf import settings + +from common.utils import get_current_domain + +from gitlab import Gitlab +from gitlab.v4.objects import Group +from gitlab.exceptions import GitlabGetError + + +from production.models import ProofsRepository +from production.constants import ( + PROOFS_REPO_UNINITIALIZED, + PROOFS_REPO_CREATED, +) + + +class Command(BaseCommand): + """ + This command handles the creation and updating of git repositories. + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # Check that the global GITLAB_ROOT constant is set + if not hasattr(settings, "GITLAB_ROOT") or settings.GITLAB_ROOT == "": + raise LookupError( + "Constant `GITLAB_ROOT` is either not present in settings file or empty, please add it." + ) + + self.GL: Gitlab = self._instanciate_gitlab() + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument( + "--id", + type=int, + required=False, + help="The submission preprint identifier to handle a specific submission, leave blank to handle all", + ) + + def _instanciate_gitlab(self) -> Gitlab: + """ + Test the connection to the git server, returns a Gitlab object. + """ + + if not hasattr(settings, "GITLAB_KEY") or settings.GITLAB_KEY == "": + raise LookupError( + "Constant `GITLAB_KEY` is either not present in secret file or empty, please add it." + ) + + if not hasattr(settings, "GITLAB_URL") or settings.GITLAB_URL == "": + raise LookupError( + "Constant `GITLAB_URL` is either not present in secret file or empty, please add it." + ) + + GL = Gitlab(url=settings.GITLAB_URL, private_token=settings.GITLAB_KEY) + + try: + GL.auth() + except Exception as e: + raise AssertionError( + "Could not authenticate with GitLab, please check your credentials." + ) from e + + return GL + + def _get_or_create_nested_group(self, group_path: str) -> Group: + """ + Create a new group on the git server based on a path of nested folders. + """ + parent_group = None + group_path_segments = group_path.split("/") + + # Traverse the path segments (up to the second-to-last one) + # and create the groups if they do not exist + for i, group_path_segment in enumerate(group_path_segments[:-1]): + path_up_to_segment_i = "/".join(group_path_segments[: i + 1]) + + # Check if group exists in the server + try: + group = self.GL.groups.get(path_up_to_segment_i) + + # If it does not exist, create it + except GitlabGetError: + # Guard against the root group not existing + if parent_group is None: + raise AssertionError( + f"The parent group of {path_up_to_segment_i} does not exist. \ + This should not happen normally (and would not be fixable \ + because GitLab does not allow root groups to be created)." + ) + + # Create the group + group = self.GL.groups.create( + { + "name": group_path_segment, + "path": group_path_segment, + "parent_id": parent_group.id, + "visibility": "private", + } + ) + + # Set the parent group to the current group + parent_group = group + + return group + + def _create_git_repo(self, repo: ProofsRepository): + """ + Create a new git repository for the submission. + """ + # Check if repo exists in the server + try: + project = self.GL.projects.get(repo.git_path) + + # Create the repo on the server + except GitlabGetError: + # Get the namespace id + parent_group_id = self._get_or_create_nested_group(repo.git_path).id + project = self.GL.projects.create( + { + "name": repo.name, + "namespace_id": parent_group_id, + "visibility": "private", + "description": "Proofs for https://{domain}/submissions/{preprint_id}".format( + domain=get_current_domain(), + preprint_id=repo.stream.submission.preprint.identifier_w_vn_nr, + ), + } + ) + + self.stdout.write( + self.style.SUCCESS(f"Created git repository at {repo.git_path}") + ) + + def handle(self, *args, **options): + # Limit the actions to a specific submission if requested + if preprint_id := options.get("id"): + repos = ProofsRepository.objects.filter( + stream__submission__preprint__identifier_w_vn_nr=preprint_id + ) + else: + repos = ProofsRepository.objects.all() + + # Create the repos + repos_to_be_created = repos.filter(status=PROOFS_REPO_UNINITIALIZED) + for repo in repos_to_be_created: + self._create_git_repo(repo) + repo.status = PROOFS_REPO_CREATED + repo.save() diff --git a/scipost_django/production/models.py b/scipost_django/production/models.py index c970ff471..f29affeb9 100644 --- a/scipost_django/production/models.py +++ b/scipost_django/production/models.py @@ -13,7 +13,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.db.models import Value from django.db.models.functions import Concat - +from django.conf import settings from .constants import ( PRODUCTION_STREAM_STATUS, @@ -300,7 +300,8 @@ class ProofsRepository(models.Model): "%Y-%m" ).split("-") - return "/Proofs/{journal}/{year}/{month}/{repo_name}".format( + return "{ROOT}/Proofs/{journal}/{year}/{month}/{repo_name}".format( + ROOT=settings.GITLAB_ROOT, journal=self.journal_path_abbrev, year=creation_year, month=creation_month, @@ -309,7 +310,10 @@ class ProofsRepository(models.Model): @property def template_path(self) -> str: - return "Templates/{journal}".format(journal=self.journal_path_abbrev) + return "{ROOT}/Templates/{journal}".format( + ROOT=settings.GITLAB_ROOT, + journal=self.journal_path_abbrev, + ) def __str__(self) -> str: return f"Proofs repo for {self.stream}" -- GitLab