From cf05965352defdf15017ed9ecfcef3383d4abb6a Mon Sep 17 00:00:00 2001 From: Ivan Kovnatsky <75213+ivankovnatsky@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:57:33 +0200 Subject: [PATCH] Add option to customize Jira priorities (#1637) * 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 2765a877dc51932294e4b4f2b93c17503680b5c5. * 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 6c8d2b1904f60facb337b95b7b5f31eee495642a. * 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 31f3d590dc9b365e48ab0aa76328dbafccd6c118. * 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 --- docs/configuration/sinks/jira.rst | 14 ++++++ .../core/sinks/jira/jira_sink_params.py | 3 +- src/robusta/integrations/jira/client.py | 45 ++++++++++++++++++- src/robusta/integrations/jira/sender.py | 33 ++++++++------ 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/docs/configuration/sinks/jira.rst b/docs/configuration/sinks/jira.rst index e76015b04..d8bd53db2 100644 --- a/docs/configuration/sinks/jira.rst +++ b/docs/configuration/sinks/jira.rst @@ -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 `__ to get your project id. * ``issue_type_id_override`` : [Optional - default: None] If available, will override the ``issue_type`` configuration. Follow these `instructions `__ to get your issue id. @@ -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] diff --git a/src/robusta/core/sinks/jira/jira_sink_params.py b/src/robusta/core/sinks/jira/jira_sink_params.py index 3e5a9561c..9824128de 100644 --- a/src/robusta/core/sinks/jira/jira_sink_params.py +++ b/src/robusta/core/sinks/jira/jira_sink_params.py @@ -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 @@ -20,6 +20,7 @@ class JiraSinkParams(SinkBaseParams): noReopenResolution: Optional[str] = "" epic: Optional[str] = "" assignee: Optional[str] = "" + priority_mapping: Optional[Dict[str, str]] = None @classmethod diff --git a/src/robusta/integrations/jira/client.py b/src/robusta/integrations/jira/client.py index e53c197fb..0e8e42a90 100644 --- a/src/robusta/integrations/jira/client.py +++ b/src/robusta/integrations/jira/client.py @@ -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 @@ -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]) @@ -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 "" @@ -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": { diff --git a/src/robusta/integrations/jira/sender.py b/src/robusta/integrations/jira/sender.py index 42d26a808..4cfcb5a3d 100644 --- a/src/robusta/integrations/jira/sender.py +++ b/src/robusta/integrations/jira/sender.py @@ -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:", @@ -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+|$)" @@ -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, )