diff --git a/README.md b/README.md index a443da30..cc5ee6e1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ docker run --rm -it baloise/gitopscli --help For detailed installation and usage instructions, visit [https://baloise.github.io/gitopscli/](https://baloise.github.io/gitopscli/). ## Git Provider Support -Currently, we support both BitBucket Server and GitHub. +Currently, we support BitBucket Server, GitHub and Gitlab. ## License [Apache-2.0](https://choosealicense.com/licenses/apache-2.0/) diff --git a/gitopscli/cliparser.py b/gitopscli/cliparser.py index f0cb76b4..58eb64e7 100644 --- a/gitopscli/cliparser.py +++ b/gitopscli/cliparser.py @@ -244,7 +244,7 @@ def __parse_yaml(value: str) -> Any: def __parse_git_provider(value: str) -> GitProvider: - mapping = {"github": GitProvider.GITHUB, "bitbucket-server": GitProvider.BITBUCKET} + mapping = {"github": GitProvider.GITHUB, "bitbucket-server": GitProvider.BITBUCKET, "gitlab": GitProvider.GITLAB} assert set(mapping.values()) == set(GitProvider), "git provider mapping not exhaustive" lowercase_stripped_value = value.lower().strip() if lowercase_stripped_value not in mapping: @@ -270,6 +270,8 @@ def __deduce_empty_git_provider_from_git_provider_url( updated_args["git_provider"] = GitProvider.GITHUB elif "bitbucket" in git_provider_url.lower(): updated_args["git_provider"] = GitProvider.BITBUCKET + elif "gitlab" in git_provider_url.lower(): + updated_args["git_provider"] = GitProvider.GITLAB else: error("Cannot deduce git provider from --git-provider-url. Please provide --git-provider") return updated_args diff --git a/gitopscli/git_api/git_provider.py b/gitopscli/git_api/git_provider.py index 8b00ac5f..f9df9e29 100644 --- a/gitopscli/git_api/git_provider.py +++ b/gitopscli/git_api/git_provider.py @@ -4,3 +4,4 @@ class GitProvider(Enum): GITHUB = auto() BITBUCKET = auto() + GITLAB = auto() diff --git a/gitopscli/git_api/git_repo_api_factory.py b/gitopscli/git_api/git_repo_api_factory.py index a32a8f4d..af6c3ddd 100644 --- a/gitopscli/git_api/git_repo_api_factory.py +++ b/gitopscli/git_api/git_repo_api_factory.py @@ -3,6 +3,7 @@ from .git_repo_api import GitRepoApi from .github_git_repo_api_adapter import GithubGitRepoApiAdapter from .bitbucket_git_repo_api_adapter import BitbucketGitRepoApiAdapter +from .gitlab_git_repo_api_adapter import GitlabGitRepoApiAdapter from .git_repo_api_logging_proxy import GitRepoApiLoggingProxy from .git_api_config import GitApiConfig from .git_provider import GitProvider @@ -29,4 +30,15 @@ def create(config: GitApiConfig, organisation: str, repository_name: str) -> Git organisation=organisation, repository_name=repository_name, ) + elif config.git_provider is GitProvider.GITLAB: + provider_url = config.git_provider_url + if not provider_url: + provider_url = "https://www.gitlab.com" + git_repo_api = GitlabGitRepoApiAdapter( + git_provider_url=provider_url, + username=config.username, + password=config.password, + organisation=organisation, + repository_name=repository_name, + ) return GitRepoApiLoggingProxy(git_repo_api) diff --git a/gitopscli/git_api/gitlab_git_repo_api_adapter.py b/gitopscli/git_api/gitlab_git_repo_api_adapter.py new file mode 100644 index 00000000..83867be7 --- /dev/null +++ b/gitopscli/git_api/gitlab_git_repo_api_adapter.py @@ -0,0 +1,82 @@ +from typing import Optional +import requests + +import gitlab +from gitopscli.gitops_exception import GitOpsException + +from .git_repo_api import GitRepoApi + + +class GitlabGitRepoApiAdapter(GitRepoApi): + def __init__( + self, + git_provider_url: str, + username: Optional[str], + password: Optional[str], + organisation: str, + repository_name: str, + ) -> None: + try: + self.__gitlab = gitlab.Gitlab(git_provider_url, private_token=password) + project = self.__gitlab.projects.get(f"{organisation}/{repository_name}") + except requests.exceptions.ConnectionError as ex: + raise GitOpsException(f"Error connecting to '{git_provider_url}''") from ex + except gitlab.exceptions.GitlabAuthenticationError as ex: + raise GitOpsException("Bad Personal Access Token") + except gitlab.exceptions.GitlabGetError as ex: + if ex.response_code == 404: + raise GitOpsException(f"Repository '{organisation}/{repository_name}' does not exist") + raise GitOpsException(f"Error getting repository: '{ex.error_message}'") + + self.__token_name = username + self.__access_token = password + self.__project = project + + def get_username(self) -> Optional[str]: + return self.__token_name + + def get_password(self) -> Optional[str]: + return self.__access_token + + def get_clone_url(self) -> str: + return str(self.__project.http_url_to_repo) + + def create_pull_request_to_default_branch( + self, from_branch: str, title: str, description: str + ) -> GitRepoApi.PullRequestIdAndUrl: + to_branch = self.__get_default_branch() + return self.create_pull_request(from_branch, to_branch, title, description) + + def create_pull_request( + self, from_branch: str, to_branch: str, title: str, description: str + ) -> GitRepoApi.PullRequestIdAndUrl: + merge_request = self.__project.mergerequests.create( + {"source_branch": from_branch, "target_branch": to_branch, "title": title, "description": description} + ) + return GitRepoApi.PullRequestIdAndUrl(pr_id=merge_request.iid, url=merge_request.web_url) + + def merge_pull_request(self, pr_id: int) -> None: + merge_request = self.__project.mergerequests.get(pr_id) + merge_request.merge() + + def add_pull_request_comment(self, pr_id: int, text: str, parent_id: Optional[int] = None) -> None: + merge_request = self.__project.mergerequests.get(pr_id) + merge_request.notes.create({"body": text}) + + def delete_branch(self, branch: str) -> None: + self.__project.branches.delete(branch) + + def get_branch_head_hash(self, branch: str) -> str: + branch_instance = self.__project.branches.get(branch) + return str(branch_instance.commit["id"]) + + def get_pull_request_branch(self, pr_id: int) -> str: + merge_request = self.__project.mergerequests.get(pr_id) + return str(merge_request.source_branch) + + def __get_default_branch(self) -> str: + branches = self.__project.branches.list() + default_branch = next(filter(lambda x: x.default, branches), None) + if default_branch is None: + raise GitOpsException(f"Default branch does not exist") + return str(default_branch.name) diff --git a/setup.py b/setup.py index 60c7b3b3..8a756068 100644 --- a/setup.py +++ b/setup.py @@ -6,5 +6,11 @@ packages=find_packages(), entry_points={"console_scripts": ["gitopscli = gitopscli.__main__:main"]}, setup_requires=["wheel"], - install_requires=["GitPython==3.0.6", "ruamel.yaml==0.16.5", "atlassian-python-api==1.14.5", "PyGithub==1.53",], + install_requires=[ + "GitPython==3.0.6", + "ruamel.yaml==0.16.5", + "atlassian-python-api==1.14.5", + "PyGithub==1.53", + "python-gitlab==2.6.0", + ], )