From 8ea039bd6c04dd6d911bee45c569aa690bb38d7e Mon Sep 17 00:00:00 2001 From: Pragya Chaudhary Date: Tue, 24 Mar 2026 15:36:43 +0530 Subject: [PATCH 1/4] Test JIRA auth changes --- cloud_governance/common/jira/jira.py | 99 +++++++++++++++---- .../common/jira/jira_operations.py | 10 +- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/cloud_governance/common/jira/jira.py b/cloud_governance/common/jira/jira.py index 81fe5ee26..181606a84 100644 --- a/cloud_governance/common/jira/jira.py +++ b/cloud_governance/common/jira/jira.py @@ -1,6 +1,7 @@ import asyncio import logging +from urllib.parse import quote import aiohttp import urllib3 @@ -31,26 +32,47 @@ def __init__( self.username = username self.ticket_queue = ticket_queue self.password = password + self.token = token + self._auth = None if not loop: self.loop = asyncio.new_event_loop() self.new_loop = True else: self.loop = loop self.new_loop = False - self.token = token - if not self.token: - if self.password: - payload = BasicAuth(self.username, self.password) - else: + + # Jira Cloud API tokens use HTTP Basic auth (email + token), same as curl -u user:token + if self.token: + if not self.username: logger.error( - "Basic Authentication expected as no token was found but password is missing" + "Basic Authentication with API token requires JIRA_USERNAME (email)" ) - raise JiraException + raise JiraException( + "Basic Authentication with API token requires JIRA_USERNAME (email)" + ) + self._auth = BasicAuth(self.username, self.token) + elif self.password: + if not self.username: + logger.error( + "Basic Authentication requires JIRA_USERNAME" + ) + raise JiraException( + "Basic Authentication requires JIRA_USERNAME" + ) + self._auth = BasicAuth(self.username, self.password) else: - payload = "Bearer: %s" % self.token - self.headers = {"Authorization": payload} - except: - pass + logger.error( + "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD" + ) + raise JiraException( + "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD" + ) + self.headers = {"Accept": "application/json"} + except JiraException: + raise + except Exception as ex: + logger.error("Failed to initialize Jira client: %s", ex) + raise JiraException from ex def __exit__(self): if self.new_loop: @@ -60,6 +82,7 @@ async def get_request(self, endpoint): logger.debug("GET: %s" % endpoint) try: async with aiohttp.ClientSession( + auth=self._auth, headers=self.headers, loop=self.loop, ) as session: @@ -81,14 +104,19 @@ async def post_request(self, endpoint, payload): logger.debug("POST: {%s:%s}" % (endpoint, payload)) try: async with aiohttp.ClientSession( - headers=self.headers, loop=self.loop + auth=self._auth, + headers=self.headers, + loop=self.loop, ) as session: async with session.post( self.url + endpoint, json=payload, verify_ssl=False, ) as response: - data = await response.json(content_type="application/json") + if response.status == 204: + data = {} + else: + data = await response.json(content_type="application/json") except Exception as ex: logger.debug(ex) logger.error("There was something wrong with your request.") @@ -98,13 +126,17 @@ async def post_request(self, endpoint, payload): return data if response.status == 404: logger.error("Resource not found: %s" % self.url + endpoint) + else: + logger.error(data) return False async def put_request(self, endpoint, payload): logger.debug("POST: {%s:%s}" % (endpoint, payload)) try: async with aiohttp.ClientSession( - headers=self.headers, loop=self.loop + auth=self._auth, + headers=self.headers, + loop=self.loop, ) as session: async with session.put( self.url + endpoint, @@ -158,11 +190,37 @@ async def create_subtask(self, parent_ticket, cloud, description, type_of_subtas response = await self.post_request(endpoint, data) return response + async def _resolve_watcher_to_account_id(self, watcher): + """ + Jira Cloud expects an Atlassian accountId in the watchers POST body (JSON string), + not an email. Resolve email or short username via REST v2 user search. + """ + if not watcher or not str(watcher).strip(): + return None + w = str(watcher).strip() + user = await self.get_user_by_email(w) + if user and user.get("accountId"): + return user["accountId"] + if "@" not in w: + user = await self.get_user_by_email(f"{w}@redhat.com") + if user and user.get("accountId"): + return user["accountId"] + logger.error("Could not resolve Jira accountId for watcher: %s", w) + return None + async def add_watcher(self, ticket, watcher): + account_id = await self._resolve_watcher_to_account_id(watcher) + if not account_id: + return False issue_id = "%s-%s" % (self.ticket_queue, ticket) endpoint = "/issue/%s/watchers" % issue_id - logger.debug("POST transition: {%s:%s}" % (issue_id, watcher)) - response = await self.post_request(endpoint, watcher) + logger.debug( + "POST watcher issue=%s accountId=%s (from %s)", + issue_id, + account_id, + watcher, + ) + response = await self.post_request(endpoint, account_id) return response async def add_label(self, ticket, label): @@ -221,15 +279,18 @@ async def get_watchers(self, ticket): return result async def get_user_by_email(self, email): - endpoint = f"/user/search?username={email}" + endpoint = f"/user/search?query={quote(email)}" logger.debug("GET user: %s" % endpoint) result = await self.get_request(endpoint) if not result: - logger.error("User not found") + logger.error("User not found for query: %s", email) return None + el = email.lower() for user in result: - if user.get("emailAddress") == email: + if (user.get("emailAddress") or "").lower() == el: return user + if len(result) == 1: + return result[0] return None async def get_pending_tickets(self): diff --git a/cloud_governance/common/jira/jira_operations.py b/cloud_governance/common/jira/jira_operations.py index 99442d6b1..93c3e6f75 100644 --- a/cloud_governance/common/jira/jira_operations.py +++ b/cloud_governance/common/jira/jira_operations.py @@ -31,10 +31,18 @@ def __init__(self): self.__jira_url = self.__environment_variables_dict.get('JIRA_URL').strip() self.__jira_username = self.__environment_variables_dict.get('JIRA_USERNAME').strip() self.__jira_token = self.__environment_variables_dict.get('JIRA_TOKEN').strip() + self.__jira_password = self.__environment_variables_dict.get('JIRA_PASSWORD', '').strip() self.__jira_queue = self.__environment_variables_dict.get('JIRA_QUEUE').strip() self.__cache_temp_dir = self.__environment_variables_dict.get('TEMPORARY_DIRECTORY', '').strip() self.__loop = asyncio.new_event_loop() - self.__jira_object = Jira(url=self.__jira_url, username=self.__jira_username, token=self.__jira_token, ticket_queue=self.__jira_queue, loop=self.__loop) + self.__jira_object = Jira( + url=self.__jira_url, + username=self.__jira_username, + password=self.__jira_password or None, + token=self.__jira_token or None, + ticket_queue=self.__jira_queue, + loop=self.__loop, + ) @typeguard.typechecked @logger_time_stamp From 9828b07b05197becb1a55c1c48c9ab25648c1a8b Mon Sep 17 00:00:00 2001 From: Pragya Chaudhary Date: Tue, 24 Mar 2026 18:53:02 +0530 Subject: [PATCH 2/4] Test JIRA auth changes --- .../run_cloud_resource_orchestration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py b/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py index 5e78881b6..9877c4e84 100644 --- a/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py +++ b/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py @@ -25,8 +25,9 @@ S3_RESULTS_PATH = os.environ['S3_RESULTS_PATH'] ATHENA_DATABASE_NAME = os.environ['ATHENA_DATABASE_NAME'] ATHENA_TABLE_NAME = os.environ['ATHENA_TABLE_NAME'] -QUAY_CLOUD_GOVERNANCE_REPOSITORY = os.environ.get('QUAY_CLOUD_GOVERNANCE_REPOSITORY', - 'quay.io/cloud-governance/cloud-governance:latest') +# QUAY_CLOUD_GOVERNANCE_REPOSITORY = os.environ.get('QUAY_CLOUD_GOVERNANCE_REPOSITORY', +# 'quay.io/cloud-governance/cloud-governance:latest') +QUAY_CLOUD_GOVERNANCE_REPOSITORY = 'quay.io/rh-ee-pragchau/cloud-governance:latest' es_index = CLOUD_RESOURCE_ORCHESTRATION_INDEX From e255841ff55cad103b8ddd1c4d5d7709cf1387f9 Mon Sep 17 00:00:00 2001 From: Pragya Chaudhary Date: Tue, 24 Mar 2026 19:02:14 +0530 Subject: [PATCH 3/4] Test JIRA auth changes --- .../run_cloud_resource_orchestration.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py b/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py index 9877c4e84..5e78881b6 100644 --- a/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py +++ b/jenkins/cloud_resource_orchestration/run_cloud_resource_orchestration.py @@ -25,9 +25,8 @@ S3_RESULTS_PATH = os.environ['S3_RESULTS_PATH'] ATHENA_DATABASE_NAME = os.environ['ATHENA_DATABASE_NAME'] ATHENA_TABLE_NAME = os.environ['ATHENA_TABLE_NAME'] -# QUAY_CLOUD_GOVERNANCE_REPOSITORY = os.environ.get('QUAY_CLOUD_GOVERNANCE_REPOSITORY', -# 'quay.io/cloud-governance/cloud-governance:latest') -QUAY_CLOUD_GOVERNANCE_REPOSITORY = 'quay.io/rh-ee-pragchau/cloud-governance:latest' +QUAY_CLOUD_GOVERNANCE_REPOSITORY = os.environ.get('QUAY_CLOUD_GOVERNANCE_REPOSITORY', + 'quay.io/cloud-governance/cloud-governance:latest') es_index = CLOUD_RESOURCE_ORCHESTRATION_INDEX From 245a44d44b0d5b1ce7736dd1522a17512818441c Mon Sep 17 00:00:00 2001 From: Pragya Chaudhary Date: Wed, 25 Mar 2026 14:40:03 +0530 Subject: [PATCH 4/4] Unittest changes --- .../mocks/mock_jira.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py b/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py index 110322299..1f414c709 100644 --- a/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py +++ b/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py @@ -2,8 +2,16 @@ from functools import wraps from unittest.mock import patch - from cloud_governance.common.jira.jira_operations import JiraOperations +from cloud_governance.main.environment_variables import environment_variables + +# Jira() is still constructed with real credentials; CI often has no JIRA_* env vars. +_JIRA_STANDIN_KEYS = ('JIRA_USERNAME', 'JIRA_URL', 'JIRA_QUEUE') +_JIRA_STANDIN_DEFAULTS = { + 'JIRA_USERNAME': 'mock@example.com', + 'JIRA_URL': 'https://mock.atlassian.net', + 'JIRA_QUEUE': 'MOCK', +} def get_ticket_response(): @@ -94,10 +102,19 @@ def method_wrapper(*args, **kwargs): @param kwargs: @return: """ - with patch.object(JiraOperations, 'get_issue', mock_get_issue),\ - patch.object(JiraOperations, 'move_issue_state', mock_move_issue_state), \ - patch.object(JiraOperations, 'get_all_issues', mock_get_all_issues): - result = method(*args, **kwargs) - return result + env = environment_variables.environment_variables_dict + saved_jira = {k: env.get(k, '') for k in _JIRA_STANDIN_KEYS} + try: + for key, default in _JIRA_STANDIN_DEFAULTS.items(): + if not (env.get(key) or '').strip(): + env[key] = default + with patch.object(JiraOperations, 'get_issue', mock_get_issue),\ + patch.object(JiraOperations, 'move_issue_state', mock_move_issue_state), \ + patch.object(JiraOperations, 'get_all_issues', mock_get_all_issues): + result = method(*args, **kwargs) + return result + finally: + for key in _JIRA_STANDIN_KEYS: + env[key] = saved_jira[key] return method_wrapper