Skip to content

Commit f7440c1

Browse files
changes to make it suitable for cloud (#375)
* Changes for issueFunction and API updation * Lint fixes and version fix and other fixes * fixes on the PR * format fixed * suggested fixes * changes in test file * changing to path variable * sql injection fix * fixes * adding paramstyle * updated the comment
1 parent 9fc7a4b commit f7440c1

File tree

6 files changed

+113
-33
lines changed

6 files changed

+113
-33
lines changed

constraints.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
snowflake_connector_python>=3.15

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
jira
1+
jira>=3.8.0
22
requests
33
requests_kerberos
44
fedmsg
55
fedora-messaging
66
PyGithub
77
pypandoc_binary
8+
dotenv
89
urllib3
910
jinja2
1011
flask
12+
snowflake
1113
webhook-to-fedora-messaging-messages
1214
ldap3>=2.9.0

sync2jira/downstream_issue.py

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
import difflib
2222
import logging
2323
import operator
24+
import os
2425
import re
2526
from typing import Any, Optional, Union
2627
import unicodedata
2728

29+
from dotenv import load_dotenv
2830
from jira import JIRAError
2931
import jira.client
3032
import pypandoc
33+
import snowflake.connector
3134

3235
import Rover_Lookup
3336
from sync2jira.intermediary import Issue, PR
3437

38+
load_dotenv()
3539
# The date the service was upgraded
3640
# This is used to ensure legacy comments are not touched
3741
UPDATE_DATE = datetime(2019, 7, 9, 18, 18, 36, 480291, tzinfo=timezone.utc)
@@ -42,6 +46,62 @@
4246
duplicate_issues_subject = "FYI: Duplicate Sync2jira Issues"
4347

4448
jira_cache = {}
49+
SNOWFLAKE_QUERY = f"""
50+
SELECT
51+
CONCAT(p.PKEY, '-', a.issue_key) AS issue_key,
52+
remote_link_url,
53+
updated
54+
FROM
55+
(
56+
SELECT
57+
ji.PROJECT_ID AS project_id,
58+
ji.ISSUENUM AS issue_key,
59+
rl.URL AS remote_link_url,
60+
ji.updated
61+
FROM
62+
JIRA_DB.MARTS.JIRA_REMOTELINK AS rl
63+
INNER JOIN JIRA_DB.MARTS.JIRA_ISSUE AS ji ON ji.ID = rl.ISSUEID
64+
AND rl.TITLE = '{remote_link_title}' AND rl.URL = ?
65+
) AS a
66+
LEFT JOIN JIRA_DB.MARTS.JIRA_PROJECT AS p on a.project_id = p.ID
67+
"""
68+
69+
70+
GH_URL_PATTERN = re.compile(r"https://github\.com/[^/]+/[^/]+/(issues|pull)/\d+")
71+
72+
73+
def validate_github_url(url):
74+
"""URL validation"""
75+
return bool(GH_URL_PATTERN.fullmatch(url))
76+
77+
78+
def get_snowflake_conn():
79+
"""Get Snowflake connection - lazy initialization"""
80+
81+
return snowflake.connector.connect(
82+
account=os.getenv("SNOWFLAKE_ACCOUNT"),
83+
user=os.getenv("SNOWFLAKE_USER"),
84+
password=os.getenv("SNOWFLAKE_PAT"),
85+
role=os.getenv("SNOWFLAKE_ROLE"),
86+
warehouse=os.getenv("SNOWFLAKE_WAREHOUSE", "DEFAULT"),
87+
database=os.getenv("SNOWFLAKE_DATABASE", "JIRA_DB"),
88+
schema=os.getenv("SNOWFLAKE_SCHEMA", "PUBLIC"),
89+
paramstyle="qmark",
90+
)
91+
92+
93+
def execute_snowflake_query(issue):
94+
if not validate_github_url(issue.url):
95+
log.error(f"Invalid GitHub URL format: {issue.url}")
96+
return []
97+
conn = get_snowflake_conn()
98+
# Execute the Snowflake query
99+
with conn as c:
100+
cursor = c.cursor()
101+
cursor.execute(SNOWFLAKE_QUERY, (issue.url,))
102+
results = cursor.fetchall()
103+
cursor.close()
104+
return results
45105

46106

47107
def check_jira_status(client):
@@ -54,11 +114,11 @@ def check_jira_status(client):
54114
:rtype: Bool
55115
"""
56116
# Search for any issue remote title
57-
ret = client.search_issues("issueFunction in linkedIssuesOfRemote('*')")
58-
if len(ret) < 1:
59-
# If we did not find anything return false
117+
try:
118+
client.server_info()
119+
return True
120+
except Exception:
60121
return False
61-
return True
62122

63123

64124
def _comment_format(comment):
@@ -126,7 +186,7 @@ def get_jira_client(issue, config):
126186
return client
127187

128188

129-
def _matching_jira_issue_query(client, issue, config, free=False):
189+
def _matching_jira_issue_query(client, issue, config):
130190
"""
131191
API calls that find matching JIRA tickets if any are present.
132192
@@ -138,14 +198,14 @@ def _matching_jira_issue_query(client, issue, config, free=False):
138198
:rtype: List
139199
"""
140200
# Searches for any remote link to the issue.url
141-
query = (
142-
f'issueFunction in linkedIssuesOfRemote("{remote_link_title}") and '
143-
f'issueFunction in linkedIssuesOfRemote("{issue.url}")'
144-
)
145-
if free:
146-
query += " and statusCategory != Done"
201+
147202
# Query the JIRA client and store the results
148-
results_of_query: jira.client.ResultList = client.search_issues(query)
203+
results = execute_snowflake_query(issue)
204+
results_of_query = []
205+
if len(results) > 0:
206+
issue_keys = (row[0] for row in results)
207+
jql = f"key in ({','.join(issue_keys)})"
208+
results_of_query = client.search_issues(jql)
149209
if len(results_of_query) > 1:
150210
final_results = []
151211
# TODO: there is pagure-specific code in here that handles the case where a dropped issue's URL is
@@ -194,7 +254,9 @@ def _matching_jira_issue_query(client, issue, config, free=False):
194254
final_results.append(results_of_query[0])
195255

196256
# Return the final_results
197-
log.debug("Found %i results for query %r", len(final_results), query)
257+
log.debug(
258+
"Found %i results for query with issue %r", len(final_results), issue.url
259+
)
198260
return final_results
199261
else:
200262
return results_of_query

sync2jira/intermediary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def map_fixVersion(mapping, issue):
278278
issue["milestone"] = fixVersion_map.replace("XXX", issue["milestone"])
279279

280280

281-
JIRA_REFERENCE = re.compile(r"\bJIRA:\s*([A-Z]+-\d+)\b")
281+
JIRA_REFERENCE = re.compile(r"\bJIRA:\s*([A-Z][A-Z0-9]*-\d+)\b")
282282

283283

284284
def matcher(content: Optional[str], comments: list[dict[str, str]]) -> str:

tests/test_downstream_issue.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ class MockIssue(object):
183183
"(resolution is null OR resolution = Duplicate)",
184184
)
185185

186+
@mock.patch(PATH + "execute_snowflake_query")
186187
@mock.patch("jira.client.JIRA")
187-
def test_get_existing_newstyle(self, client):
188+
def test_get_existing_newstyle(self, client, mock_snowflake):
188189
config = self.mock_config
189190

190191
issue = MagicMock()
@@ -195,13 +196,13 @@ def test_get_existing_newstyle(self, client):
195196
mock_results_of_query.fields.summary = "A title, a title..."
196197

197198
client.return_value.search_issues.return_value = [mock_results_of_query]
199+
mock_snowflake.return_value = [("SYNC2JIRA-123",)]
198200
result = d._get_existing_jira_issue(jira.client.JIRA(), issue, config)
199201
# Ensure that we get the mock_result_of_query as a result
200202
self.assertEqual(result, mock_results_of_query)
201203

202204
client.return_value.search_issues.assert_called_once_with(
203-
'issueFunction in linkedIssuesOfRemote("Upstream issue") and '
204-
'issueFunction in linkedIssuesOfRemote("http://threebean.org")'
205+
"key in (SYNC2JIRA-123)"
205206
)
206207

207208
@mock.patch("jira.client.JIRA")
@@ -1390,8 +1391,10 @@ def test_verify_tags(self):
13901391
@mock.patch(PATH + "find_username")
13911392
@mock.patch(PATH + "check_comments_for_duplicate")
13921393
@mock.patch("jira.client.JIRA")
1394+
@mock.patch(PATH + "execute_snowflake_query")
13931395
def test_matching_jira_issue_query(
13941396
self,
1397+
mock_snowflake,
13951398
mock_client,
13961399
mock_check_comments_for_duplicates,
13971400
mock_find_username,
@@ -1412,6 +1415,7 @@ def test_matching_jira_issue_query(
14121415
]
14131416
mock_check_comments_for_duplicates.return_value = True
14141417
mock_find_username.return_value = "mock_username"
1418+
mock_snowflake.return_value = [("SYNC2JIRA-123",)]
14151419

14161420
# Call the function
14171421
response = d._matching_jira_issue_query(
@@ -1420,10 +1424,7 @@ def test_matching_jira_issue_query(
14201424

14211425
# Assert everything was called correctly
14221426
self.assertEqual(response, [mock_downstream_issue])
1423-
mock_client.search_issues.assert_called_with(
1424-
'issueFunction in linkedIssuesOfRemote("Upstream issue")'
1425-
' and issueFunction in linkedIssuesOfRemote("mock_url")'
1426-
)
1427+
mock_client.search_issues.assert_called_with("key in (SYNC2JIRA-123)")
14271428
mock_check_comments_for_duplicates.assert_called_with(
14281429
mock_client, mock_downstream_issue, "mock_username"
14291430
)
@@ -1567,35 +1568,29 @@ def test_check_jira_status_false(self):
15671568
"""
15681569
This function tests 'check_jira_status' where we return false
15691570
"""
1570-
# Set up return values
1571+
# Set up mock jira client that raises an exception
15711572
mock_jira_client = MagicMock()
1572-
mock_jira_client.search_issues.return_value = []
1573+
mock_jira_client.server_info.side_effect = Exception("Connection failed")
15731574

15741575
# Call the function
15751576
response = d.check_jira_status(mock_jira_client)
15761577

15771578
# Assert everything was called correctly
15781579
self.assertEqual(response, False)
1579-
mock_jira_client.search_issues.assert_called_with(
1580-
"issueFunction in linkedIssuesOfRemote('*')"
1581-
)
15821580

15831581
def test_check_jira_status_true(self):
15841582
"""
1585-
This function tests 'check_jira_status' where we return false
1583+
This function tests 'check_jira_status' where we return true
15861584
"""
1587-
# Set up return values
1585+
# Set up mock jira client that works normally
15881586
mock_jira_client = MagicMock()
1589-
mock_jira_client.search_issues.return_value = ["some", "values"]
1587+
mock_jira_client.server_info.return_value = {"version": "8.0.0"}
15901588

15911589
# Call the function
15921590
response = d.check_jira_status(mock_jira_client)
15931591

15941592
# Assert everything was called correctly
15951593
self.assertEqual(response, True)
1596-
mock_jira_client.search_issues.assert_called_with(
1597-
"issueFunction in linkedIssuesOfRemote('*')"
1598-
)
15991594

16001595
def test_update_on_close_update(self):
16011596
"""
@@ -1761,3 +1756,22 @@ def test_remove_diacritics(self):
17611756
for text, expected in scenarios:
17621757
actual = remove_diacritics(text)
17631758
self.assertEqual(actual, expected)
1759+
1760+
@mock.patch(PATH + "snowflake.connector.connect")
1761+
def test_execute_snowflake_query_real_connection(self, mock_snowflake_connect):
1762+
"""Test execute_snowflake_query function."""
1763+
# Create a mock issue
1764+
mock_issue = MagicMock()
1765+
mock_issue.url = "https://github.com/test/repo/issues/1"
1766+
# Call the function
1767+
result = d.execute_snowflake_query(mock_issue)
1768+
mock_cursor = (
1769+
mock_snowflake_connect.return_value.__enter__.return_value.cursor.return_value
1770+
)
1771+
# Assert the function was called correctly
1772+
mock_snowflake_connect.assert_called_once()
1773+
mock_cursor.execute.assert_called_once()
1774+
mock_cursor.fetchall.assert_called_once()
1775+
mock_cursor.close.assert_called_once()
1776+
# Assert the result
1777+
self.assertEqual(result, mock_cursor.fetchall.return_value)

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ basepython =
1212
deps =
1313
-r{toxinidir}/requirements.txt
1414
-r{toxinidir}/test-requirements.txt
15+
-c{toxinidir}/constraints.txt
1516
sitepackages = True
1617
allowlist_externals = /usr/bin/flake8,/usr/bin/black
1718
commands =

0 commit comments

Comments
 (0)