Repository: keephq/keep
Pull request: feat: add provider for GitLab
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #868 
/claim #868 

## 📑 Description
<!-- Add a brief description of the pr -->
- Integrate gitlab REST API as a provider.
- Update documentations accordingly.
<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->

## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [x] My pull request adheres to the code style of this project
- [x] My code requires changes to the documentation
- [x] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Problem statement:
New Provider: GitLab on-prem
**Describe the provider you want to add**
Gitlab could be used for ticketing like Jira.

**Describe your use case**
We use Gitlab for for all of our planning and tickets as all of our repos are there.

**Are you already using Keep?**
Yes, we plan to implement it.

Hints from issue discussion:
/bounty 50
I would like to attempt it @shahargl 
Please focus on:
1. Authentication Methods
2. Validating Scopes
3. Opening a ticket
4. Documentation on how to connect GitLab provider

Please take Jira Provider as an example.
@35C4n0r assigned. Please read my previous comment on what to focus on.
/attempt #868

<div id="algora" />

<details>
  <summary>Options</summary>
  <ul>
    <li>
      <a href="https://console.algora.io/api/bounties/clt5xote80002le0fwvp0yu36/cancel-attempt">
        Cancel my attempt
      </a>
    </li>
  </ul>
</details>

Commit message: feat: add provider for GitLab

- Integrate gitlab REST API as a provider.
- Update documentations accordingly.

Signed-off-by: Jay <jaykumar20march@gmail.com>

Patch to review:
diff --git a/README.md b/README.md
index d3b97a0248..0aa5f09e39 100644
--- a/README.md
+++ b/README.md
@@ -167,6 +167,8 @@ Workflow triggers can either be executed manually when an alert is activated or
 <p align="center">
     <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/jira-icon.png?raw=true"/>
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+    <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/gitlab-icon.png?raw=true"/>
+    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/trello-icon.png?raw=true"/>
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/github-icon.png?raw=true"/>
diff --git a/docs/providers/documentation/gitlab-provider.mdx b/docs/providers/documentation/gitlab-provider.mdx
new file mode 100644
index 0000000000..3599040c38
--- /dev/null
+++ b/docs/providers/documentation/gitlab-provider.mdx
@@ -0,0 +1,39 @@
+---
+title: "GitLab Provider"
+sidebarTitle: "GitLab Provider"
+description: "GitLab provider is a provider used to query data and creating issues in GitLab"
+---
+
+## Inputs
+
+The `query` function take following parameters as inputs:
+
+- `host` (required): GitLab host name of the project.
+- `PAT` (required): Your Personal Access Token with proper scopes.
+
+The `notify` function take following parameters as inputs:
+
+- `id` (required): The global ID or path of the project.
+- `title` (required): Title of the Issue/Ticket.
+- `description` (optional): Description for the Issue.
+- `labels` (optional): Issue labels seperated by a Comma.
+- `issue_type` (optional): Issue type name. One of `issue`, `incident`, `test_case` or `task`. Default is `issue`.
+
+## Outputs
+
+## Authentication Parameters
+
+The `notify` function requires an `personal_access_token` from GtLab.
+
+## Connecting with the Provider
+
+1. Go to https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token to see how to create a personal_access_token.
+2. Give your access token proper scope permissions
+3. Get `host`, eg: if you're using Cloud GitLab, use: `https://gitlab.com` or use your `host` if you're using onPrem.
+
+## Notes
+
+## Useful Links
+
+- https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
+- https://docs.gitlab.com/ee/api/issues.html#new-issue
diff --git a/keep-ui/public/icons/gitlab-icon.png b/keep-ui/public/icons/gitlab-icon.png
new file mode 100644
index 0000000000..b4c82efed9
Binary files /dev/null and b/keep-ui/public/icons/gitlab-icon.png differ
diff --git a/keep/providers/gitlab_provider/__init__.py b/keep/providers/gitlab_provider/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py
new file mode 100644
index 0000000000..b9a92583f3
--- /dev/null
+++ b/keep/providers/gitlab_provider/gitlab_provider.py
@@ -0,0 +1,193 @@
+"""
+GitlabProvider is a class that implements the BaseProvider interface for GitLab updates.
+"""
+
+import dataclasses
+import urllib.parse
+
+import pydantic
+import requests
+from requests import HTTPError
+
+from keep.contextmanager.contextmanager import ContextManager
+from keep.providers.base.base_provider import BaseProvider
+from keep.providers.models.provider_config import ProviderConfig, ProviderScope
+
+
+@pydantic.dataclasses.dataclass
+class GitlabProviderAuthConfig:
+    """GitLab authentication configuration."""
+
+    host: str = dataclasses.field(
+        metadata={
+            "required": True,
+            "description": "GitLab Host",
+            "sensitive": False,
+            "hint": "example.gitlab.com",
+        }
+    )
+
+    personal_access_token: str = dataclasses.field(
+        metadata={
+            "required": True,
+            "description": "GitLab Personal/Project/Group Access Tokens",
+            "sensitive": True,
+            "documentation_url": "https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html",
+        }
+    )
+
+
+class GitlabProvider(BaseProvider):
+    """Enrich alerts with GitLab tickets."""
+
+    PROVIDER_SCOPES = [
+        ProviderScope(
+            name="authenticated",
+            description="Authenticated with GitLab",
+            mandatory=True,
+            alias="GitLab PAT",
+        ),
+    ]
+    PROVIDER_TAGS = ["ticketing"]
+    PROVIDER_DISPLAY_NAME = "GitLab"
+
+    def __init__(
+            self, context_manager: ContextManager, provider_id: str, config: ProviderConfig
+    ):
+        self._host = None
+        super().__init__(context_manager, provider_id, config)
+
+    def validate_scopes(self):
+        """
+        Validate that the provider has the required scopes.
+        """
+
+        headers = {
+            "Accept": "application/json",
+            "Authorization": f"Bearer {self.authentication_config.personal_access_token}",
+        }
+
+        # first, validate user/api token are correct:
+        resp = requests.get(
+            f"{self.gitlab_host}/api/v4/projects",
+            headers=headers,
+            verify=False,
+        )
+        try:
+            resp.raise_for_status()
+            scopes = {
+                "authenticated": True
+            }
+        except HTTPError as e:
+            scopes = {
+                "authenticated": str(e)
+            }
+        return scopes
+
+    def validate_config(self):
+        self.authentication_config = GitlabProviderAuthConfig(
+            **self.config.authentication
+        )
+
+    @property
+    def gitlab_host(self):
+        # if not the first time, return the cached host
+        if self._host:
+            return self._host.rstrip("/")
+
+        # if the user explicitly supplied a host with http/https, use it
+        if self.authentication_config.host.startswith(
+                "http://"
+        ) or self.authentication_config.host.startswith("https://"):
+            self._host = self.authentication_config.host
+            return self.authentication_config.host.rstrip("/")
+
+        # otherwise, try to use https:
+        try:
+            requests.get(
+                f"https://{self.authentication_config.host}",
+                verify=False,
+            )
+            self.logger.debug("Using https")
+            self._host = f"https://{self.authentication_config.host}"
+            return self._host.rstrip("/")
+        except requests.exceptions.SSLError:
+            self.logger.debug("Using http")
+            self._host = f"http://{self.authentication_config.host}"
+            return self._host.rstrip("/")
+        # should happen only if the user supplied invalid host, so just let validate_config fail
+        except Exception:
+            return self.authentication_config.host.rstrip("/")
+
+    def dispose(self):
+        """
+        No need to dispose of anything, so just do nothing.
+        """
+        pass
+
+    def __get_auth_header(self):
+        """
+        Helper method to build the auth payload for gitlab api requests.
+        """
+        return {
+            "Authorization": f"Bearer {self.authentication_config.personal_access_token}"
+        }
+
+    # @staticmethod
+    def __build_params_from_kwargs(self, kwargs: dict):
+        params = dict()
+        for param in kwargs:
+            if isinstance(kwargs[param], list):
+                params[param] = ",".join(kwargs[param])
+            else:
+                params[param] = kwargs[param]
+        return params
+
+    def _notify(self, id: str, title: str, description: str = "", labels: str = "", issue_type: str = "issue",
+                **kwargs: dict):
+        id = urllib.parse.quote(id, safe='')
+        print(id)
+        params = self.__build_params_from_kwargs(
+            kwargs={**kwargs, 'title': title, 'description': description, 'labels': labels, 'issue_type': issue_type})
+        print(self.gitlab_host)
+        resp = requests.post(f"{self.gitlab_host}/api/v4/projects/{id}/issues", headers=self.__get_auth_header(),
+                             params=params)
+        try:
+            resp.raise_for_status()
+        except HTTPError as e:
+            raise Exception(f"Failed to create issue: {str(e)}")
+        return resp.json()
+
+
+if __name__ == "__main__":
+    # Output debug messages
+    import logging
+
+    logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()])
+    context_manager = ContextManager(
+        tenant_id="singletenant",
+        workflow_id="test",
+    )
+    # Load environment variables
+    import os
+
+    gitlab_pat = os.environ.get("GITLAB_PAT")
+    gitlab_host = os.environ.get("GITLAB_HOST")
+
+    # Initalize the provider and provider config
+    config = ProviderConfig(
+        description="GitLab Provider",
+        authentication={
+            "personal_access_token": gitlab_pat,
+            "host": gitlab_host,
+        },
+    )
+    provider = GitlabProvider(context_manager, provider_id="gitlab", config=config)
+    scopes = provider.validate_scopes()
+    # Create ticket
+    provider.notify(
+        board_name="KEEP board",
+        issue_type="Task",
+        summary="Test Alert",
+        description="Test Alert Description",
+    )


Code review comments:
diff_hunk: @@ -0,0 +1,39 @@
+---
+title: "GitLab Provider"
+sidebarTitle: "GitLab Provider"
+description: "GitLab provider is a provider used to query data and creating issues in GitLab"
+---
+
+## Inputs
+
+The `query` function take following parameters as inputs:
path: docs/providers/documentation/gitlab-provider.mdx
text: @user1:
there is no query in the provider itself

diff_hunk: @@ -0,0 +1,39 @@
+---
+title: "GitLab Provider"
+sidebarTitle: "GitLab Provider"
+description: "GitLab provider is a provider used to query data and creating issues in GitLab"
+---
+
+## Inputs
+
+The `query` function take following parameters as inputs:
+
+- `host` (required): GitLab host name of the project.
+- `PAT` (required): Your Personal Access Token with proper scopes.
+
+The `notify` function take following parameters as inputs:
+
+- `id` (required): The global ID or path of the project.
+- `title` (required): Title of the Issue/Ticket.
+- `description` (optional): Description for the Issue.
+- `labels` (optional): Issue labels seperated by a Comma.
+- `issue_type` (optional): Issue type name. One of `issue`, `incident`, `test_case` or `task`. Default is `issue`.
+
+## Outputs
+
+## Authentication Parameters
+
+The `notify` function requires an `personal_access_token` from GtLab.
path: docs/providers/documentation/gitlab-provider.mdx
text: @user1:
type in GtLab

also please add basic instruction how to get these
also - authentication is not part of notify

diff_hunk: @@ -0,0 +1,39 @@
+---
+title: "GitLab Provider"
+sidebarTitle: "GitLab Provider"
+description: "GitLab provider is a provider used to query data and creating issues in GitLab"
+---
+
+## Inputs
+
+The `query` function take following parameters as inputs:
+
+- `host` (required): GitLab host name of the project.
+- `PAT` (required): Your Personal Access Token with proper scopes.
path: docs/providers/documentation/gitlab-provider.mdx
text: @user1:
these are part of the authentication and not query