Skip to content

Commit

Permalink
Add option to customize Jira priorities (#1637)
Browse files Browse the repository at this point in the history
* Add option to parametries custom Jira priorities

* Build and push image to my own ghcr repo

* Update Jira documentation

* Revert "Build and push image to my own ghcr repo"

This reverts commit 2765a87.

* Put back changed before autofmt

* Fallback to using priority id when priority is not found

* Revert "Revert "Build and push image to my own ghcr repo""

This reverts commit 6c8d2b1.

* Correct fallback mechanism for  priority IDs

* Add constants for jira integration

* Correct FindingSeverity import

* Correct HTTP method parameter in Jira API calls

Fix TypeError in process_request() where method parameter was being passed
twice - once as positional argument and once in kwargs. Standardize to
use positional argument only.

Error: "process_request() got multiple values for argument 'method'"

* Use HttpMethod enum instead of string in Jira API calls

* Correct jira add attachments method

* Fix json structure for payload

* Return back url and endpoint for def create_issue

* Fix priority handling cascade with fallback logic

* Remove specific if condition for HttpMethod code and text

* Use more generate approach Exception

* Make sure _call_jira_api returns HTTPError

* Revert back auto fmt apply

* Correct indentations for create_issue comment

* Create _resolve_priority method

* Put back else clause in manage_issue method

* Add a test call to validate common priority

* Correct _resolve_priority method comment

* Revert "Revert "Revert "Build and push image to my own ghcr repo"""

This reverts commit 31f3d59.

* Simplify Jira priority name mapping

* Only validate priorities when logging level is DEBUG

* Fallback to creating Jira issues without priority if setting priority fails

When creating a Jira issue, if setting the priority fails, retry without
setting the priority.

* Revert to handle only two cases: used defined and default priority names

* Remove _create_issue_payload and _handle_attachment_and_return methods

* Revert to return None when Jira API error occurs
  • Loading branch information
ivankovnatsky authored Dec 10, 2024
1 parent 4aa9869 commit cf05965
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 16 deletions.
14 changes: 14 additions & 0 deletions docs/configuration/sinks/jira.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ Prerequisites
Optional Settings
---------------------------
* ``issue_type`` : [Optional - default: ``Task``] Jira ticket type
* ``priority_mapping`` : [Optional] Maps Robusta severity levels to Jira priorities. Example:
.. code-block:: yaml
priority_mapping:
HIGH: "High"
MEDIUM: "Medium"
LOW: "Low"
INFO: "Lowest"
* ``dedups`` : [Optional - default: ``fingerprint``] Tickets deduplication parameter. By default, Only one issue per ``fingerprint`` will be created. There can be more than one value to use. Possible values are: fingerprint, cluster_name, title, node, type, source, namespace, creation_date etc
* ``project_type_id_override`` : [Optional - default: None] If available, will override the ``project_name`` configuration. Follow these `instructions <https://confluence.atlassian.com/jirakb/how-to-get-project-id-from-the-jira-user-interface-827341414.html>`__ to get your project id.
* ``issue_type_id_override`` : [Optional - default: None] If available, will override the ``issue_type`` configuration. Follow these `instructions <https://confluence.atlassian.com/jirakb/finding-the-id-for-issue-types-646186508.html>`__ to get your issue id.
Expand Down Expand Up @@ -59,6 +68,11 @@ Configuring the Jira sink
assignee: user_id of the assignee(OPTIONAL)
epic: epic_id(OPTIONAL)
project_name: project_name
priority_mapping: (OPTIONAL)
HIGH: "High"
MEDIUM: "Medium"
LOW: "Low"
INFO: "Lowest"
scope:
include:
- identifier: [CPUThrottlingHigh, KubePodCrashLooping]
Expand Down
3 changes: 2 additions & 1 deletion src/robusta/core/sinks/jira/jira_sink_params.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Dict, List, Optional

from robusta.core.sinks.sink_base_params import SinkBaseParams
from robusta.core.sinks.sink_config import SinkConfigBase
Expand All @@ -20,6 +20,7 @@ class JiraSinkParams(SinkBaseParams):
noReopenResolution: Optional[str] = ""
epic: Optional[str] = ""
assignee: Optional[str] = ""
priority_mapping: Optional[Dict[str, str]] = None


@classmethod
Expand Down
45 changes: 44 additions & 1 deletion src/robusta/integrations/jira/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from typing import Optional
from typing import Optional, Dict

from requests.auth import HTTPBasicAuth
from requests.exceptions import HTTPError
from requests_toolbelt import MultipartEncoder

from robusta.core.reporting.base import FindingStatus
Expand Down Expand Up @@ -58,6 +59,24 @@ def __init__(self, jira_params: JiraSinkParams):
f"Jira initialized successfully. Project: {self.default_project_id} issue type: {self.default_issue_type_id}"
)

if jira_params.priority_mapping:
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
self._validate_priorities(jira_params.priority_mapping)

def _validate_priorities(self, priority_mapping: Dict[str, str]) -> None:
"""Validate that configured priorities exist in Jira"""
endpoint = "priority"
url = self._get_full_jira_url(endpoint)
available_priorities = self._call_jira_api(url) or []
available_priority_names = {p.get("name") for p in available_priorities}

for severity, priority in priority_mapping.items():
if priority not in available_priority_names:
logging.warning(
f"Configured priority '{priority}' for severity '{severity}' "
f"is not available in Jira. Available priorities: {available_priority_names}"
)

def _get_full_jira_url(self, endpoint: str) -> str:
return "/".join([self.params.url, _API_PREFIX, endpoint])

Expand Down Expand Up @@ -160,6 +179,23 @@ def _get_default_project_id(self):
return default_issue["id"]
return None

def _resolve_priority(self, priority_name: str) -> dict:
"""Resolve Jira priority:
1. User configured priority mapping (if defined)
2. Fallback to current behavior (use priority name as-is)
Returns:
dict: Priority field in format {"name": str}
"""
# 1. Try user configured priority mapping
if hasattr(self, "params") and self.params.priority_mapping:
for severity, mapped_name in self.params.priority_mapping.items():
if mapped_name == priority_name:
return {"name": mapped_name}

# 2. Fallback to current behavior
return {"name": priority_name}

def list_issues(self, search_params: Optional[str] = None):
endpoint = "search"
search_params = search_params or ""
Expand Down Expand Up @@ -208,6 +244,13 @@ def comment_issue(self, issue_id, text):
def create_issue(self, issue_data, issue_attachments=None):
endpoint = "issue"
url = self._get_full_jira_url(endpoint)

# Add priority resolution if it exists
if "priority" in issue_data:
priority_name = issue_data["priority"].get("name")
if priority_name:
issue_data["priority"] = self._resolve_priority(priority_name)

payload = {
"update": {},
"fields": {
Expand Down
33 changes: 19 additions & 14 deletions src/robusta/integrations/jira/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
from robusta.core.sinks.jira.jira_sink_params import JiraSinkParams
from robusta.integrations.jira.client import JiraClient

SEVERITY_JIRA_ID = {
FindingSeverity.HIGH: "Critical",
FindingSeverity.MEDIUM: "Major",
FindingSeverity.LOW: "Minor",
FindingSeverity.INFO: "Minor",
}

SEVERITY_EMOJI_MAP = {
FindingSeverity.HIGH: ":red_circle:",
FindingSeverity.MEDIUM: ":large_orange_circle:",
Expand All @@ -30,13 +37,6 @@
FindingSeverity.LOW: "#ffdc06",
FindingSeverity.INFO: "#05aa01",
}
# Jira priorities, see: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-priorities/#api-group-issue-priorities
SEVERITY_JIRA_ID = {
FindingSeverity.HIGH: "Critical",
FindingSeverity.MEDIUM: "Major",
FindingSeverity.LOW: "Minor",
FindingSeverity.INFO: "Minor",
}

STRONG_MARK_REGEX = r"\*{1}[\w|\s\d%!><=\-:;@#$%^&()\.\,\]\[\\\/'\"]+\*{1}"
ITALIAN_MARK_REGEX = r"(^|\s+)_{1}[\w|\s\d%!*><=\-:;@#$%^&()\.\,\]\[\\\/'\"]+_{1}(\s+|$)"
Expand Down Expand Up @@ -237,16 +237,21 @@ def send_finding_to_jira(
FindingStatus.RESOLVED if finding.title.startswith("[RESOLVED]") else FindingStatus.FIRING
)

# Default priority is "Major" if not a standard severity is given
# Use user priority mapping if available, otherwise fall back to default
severity = SEVERITY_JIRA_ID.get(finding.severity, "Major")
if self.params.priority_mapping:
severity = self.params.priority_mapping.get(finding.severity.name, severity)

issue_data = {
"description": {"type": "doc", "version": 1, "content": actions + output_blocks},
"summary": finding.title,
"labels": labels,
"priority": {"name": severity},
}

# Let client.manage_issue handle the fallback to ID if name fails
self.client.manage_issue(
{
"description": {"type": "doc", "version": 1, "content": actions + output_blocks},
"summary": finding.title,
"labels": labels,
"priority": {"name": severity},
},
issue_data,
{"status": status, "source": finding.source},
file_blocks,
)

0 comments on commit cf05965

Please sign in to comment.