From cf7255ea50d2ed2c049fbd9f2a961eed131003ca Mon Sep 17 00:00:00 2001 From: Dusty Mabe Date: Mar 14 2019 03:20:34 +0000 Subject: downstream: Add ability to use different jira instances There are a few changes in this patch set. First we'd like to support different github/pagure repos being able to be synced with different instances of JIRA. We currently have two within Red Hat that we are using, but this strategy should also support more than two. Summary of changes in this patch: - add support for multiple jira instances to the config under the **config['sync2jira']['jira'] config location. For example if I wanted to grab the auth info for the pnt-jira I would access **config['sync2jira']['jira']['pnt-jira']. - add support for a 'default_jira_instance' config key that allows for a user to specify which jira in the jira dict to use as a default in case config entries don't specify a jira instance to use. - switch function names from functions that that are only called in this file to have a preceding underscore (func -> _func) - make the only functions that get called from outside this file to be the ones that create a client connection to the jira instance - pass the jira client object around to the private functions --- diff --git a/sync2jira/downstream.py b/sync2jira/downstream.py index fd7b008..6da14a0 100644 --- a/sync2jira/downstream.py +++ b/sync2jira/downstream.py @@ -24,15 +24,40 @@ import logging import arrow import jira.client +from sync2jira.intermediary import Issue + log = logging.getLogger(__name__) remote_link_title = "Upstream issue" jira_cache = {} +def _get_jira_client(issue, config): + + # The name of the jira instance to use is stored under the 'map' + # key in the config where each upstream is mapped to jira projects. + # It is conveniently added to the Issue object from intermediary.py + # so we can use it here: + + if not isinstance(issue, Issue): + log.error("passed in issue is not an Issue instance") + log.error("It is a %s" % (type(issue).__name__)) + raise + + # Use the Jira instance set in the issue config. If none then + # use the configured default jira instance. + jira_instance = issue.downstream.get('jira_instance', False) + if not jira_instance: + jira_instance = config['sync2jira'].get('default_jira_instance', False) + if not jira_instance: + log.error("No jira_instance for issue and there is no default in the config") + raise -def _matching_jira_issue_query(issue, config, free=False): - client = jira.client.JIRA(**config['sync2jira']['jira']) + + client = jira.client.JIRA(**config['sync2jira']['jira'][jira_instance]) + return client + +def _matching_jira_issue_query(client, issue, config, free=False): query = 'issueFunction in linkedIssuesOfRemote("%s") and ' \ 'issueFunction in linkedIssuesOfRemote("%s")' % ( remote_link_title, issue.url) @@ -43,19 +68,19 @@ def _matching_jira_issue_query(issue, config, free=False): return results -def get_existing_jira_issue(issue, config): +def _get_existing_jira_issue(client, issue, config): """ Get a jira issue by the linked remote issue. This is the new supported way of doing this. """ - results = _matching_jira_issue_query(issue, config) + results = _matching_jira_issue_query(client, issue, config) if results: return results[0] else: return None -def get_existing_jira_issue_legacy(issue, config): +def _get_existing_jira_issue_legacy(client, issue, config): """ This is our old way of matching issues: use the special url field. This will be phased out and removed in a future release. @@ -65,7 +90,6 @@ def get_existing_jira_issue_legacy(issue, config): kwargs["External issue URL"] = "%s" % issue.url kwargs = sorted(kwargs.items(), key=operator.itemgetter(0)) - client = jira.client.JIRA(**config['sync2jira']['jira']) query = " AND ".join([ "=".join(["'%s'" % k, "'%s'" % v]) for k, v in kwargs if v is not None @@ -77,10 +101,9 @@ def get_existing_jira_issue_legacy(issue, config): return None -def _attach_link(config, downstream, remote_link): +def _attach_link(client, downstream, remote_link): log.info(" Attaching tracking link %r to %r", remote_link, downstream.key) modified_desc = downstream.fields.description + " " - client = jira.client.JIRA(**config['sync2jira']['jira']) # This is crazy. Querying for application links requires admin perms which # we don't have, so duckpunch the client to think it has already made the @@ -99,7 +122,7 @@ def _attach_link(config, downstream, remote_link): return downstream -def upgrade_jira_issue(downstream, issue, config): +def _upgrade_jira_issue(client, downstream, issue, config): """ Given an old legacy-style downstream issue... ...upgrade it to a new-style issue. @@ -112,16 +135,15 @@ def upgrade_jira_issue(downstream, issue, config): # Do it! remote_link = dict(url=issue.url, title=remote_link_title) - _attach_link(config, downstream, remote_link) + _attach_link(client, downstream, remote_link) -def create_jira_issue(issue, config): +def _create_jira_issue(client, issue, config): log.info(" Creating %r issue for %r", issue.downstream, issue) if config['sync2jira']['testing']: log.info(" Testing flag is true. Skipping actual creation.") return - client = jira.client.JIRA(**config['sync2jira']['jira']) kwargs = dict( summary=issue.title, description=issue.url, @@ -142,13 +164,12 @@ def create_jira_issue(issue, config): downstream = client.create_issue(**kwargs) remote_link = dict(url=issue.url, title=remote_link_title) - _attach_link(config, downstream, remote_link) + _attach_link(client, downstream, remote_link) return downstream -def close_as_duplicate(duplicate, keeper, config): +def _close_as_duplicate(client, duplicate, keeper, config): log.info(" Closing %s as duplicate of %s", duplicate.permalink(), keeper.permalink()) - client = jira.client.JIRA(**config['sync2jira']['jira']) if config['sync2jira']['testing']: log.info(" Testing flag is true. Skipping actual delete.") return @@ -183,8 +204,11 @@ def close_as_duplicate(duplicate, keeper, config): def close_duplicates(issue, config): + # Create a client connection for this issue + client = _get_jira_client(issue, config) + log.info("Looking for dupes of upstream %s, %s", issue.url, issue.title) - results = _matching_jira_issue_query(issue, config, free=True) + results = _matching_jira_issue_query(client, issue, config, free=True) if len(results) <= 1: log.info(" No duplicates found.") return @@ -192,16 +216,19 @@ def close_duplicates(issue, config): results = sorted(results, key=lambda x: arrow.get(x.fields.created)) keeper, duplicates = results[0], results[1:] for duplicate in duplicates: - close_as_duplicate(duplicate, keeper, config) + _close_as_duplicate(client, duplicate, keeper, config) def sync_with_jira(issue, config): log.info("Considering upstream %s, %s", issue.url, issue.title) + # Create a client connection for this issue + client = _get_jira_client(issue, config) + # First, check to see if we have a matching issue using the new method. # If we do, then just bail out. No sync needed. log.info(" Looking for matching downstream issue via new method.") - existing = get_existing_jira_issue(issue, config) + existing = _get_existing_jira_issue(client, issue, config) if existing: log.info(" Found existing, matching downstream %r.", existing.key) return @@ -210,7 +237,7 @@ def sync_with_jira(issue, config): # is nothing left to do than to but to create the issue and return. if not config['sync2jira'].get('legacy_matching', True): log.debug(" Legacy matching disabled.") - create_jira_issue(issue, config) + _create_jira_issue(client, issue, config) return # Otherwise, if we *are* configured to do legacy matching, then try and @@ -218,8 +245,8 @@ def sync_with_jira(issue, config): # - If we can't find it, create it. # - If we can find it, upgrade it to the new method. log.info(" Looking for matching downstream issue via legacy method.") - match = get_existing_jira_issue_legacy(issue, config) + match = _get_existing_jira_issue_legacy(client, issue, config) if not match: - create_jira_issue(issue, config) + _create_jira_issue(client, issue, config) else: - upgrade_jira_issue(match, issue, config) + _upgrade_jira_issue(client, match, issue, config) diff --git a/tests/test_downstream.py b/tests/test_downstream.py index cdf0fc2..08693c4 100644 --- a/tests/test_downstream.py +++ b/tests/test_downstream.py @@ -4,6 +4,7 @@ import mock import unittest import sync2jira.downstream as d +import jira.client class TestDownstream(unittest.TestCase): @@ -25,7 +26,7 @@ class TestDownstream(unittest.TestCase): # Ensure that we get results back from the jira client. target1 = "target1" client.return_value.search_issues = mock.MagicMock(return_value=[target1]) - result = d.get_existing_jira_issue_legacy(issue, config) + result = d._get_existing_jira_issue_legacy(jira.client.JIRA(), issue, config) eq_(result, target1) client.return_value.search_issues.assert_called_once_with( @@ -45,7 +46,7 @@ class TestDownstream(unittest.TestCase): target1 = "target1" issue = MockIssue() client.return_value.search_issues = mock.MagicMock(return_value=[target1]) - result = d.get_existing_jira_issue(issue, config) + result = d._get_existing_jira_issue(jira.client.JIRA(), issue, config) eq_(result, target1) client.return_value.search_issues.assert_called_once_with( @@ -65,7 +66,7 @@ class TestDownstream(unittest.TestCase): issue = MockIssue() client_obj = mock.MagicMock() client.return_value = client_obj - d.upgrade_jira_issue(downstream, issue, config) + d._upgrade_jira_issue(jira.client.JIRA(), downstream, issue, config) remote = { 'url': 'http://threebean.org', 'title': 'Upstream issue', @@ -91,7 +92,7 @@ class TestDownstream(unittest.TestCase): url = 'http://threebean.org' config['sync2jira']['testing'] = False - result = d.create_jira_issue(MockIssue(), config) + result = d._create_jira_issue(jira.client.JIRA(), MockIssue(), config) eq_(result, target1) client.return_value.create_issue.assert_called_with( components=[{'name': 'alsoawesome'}], @@ -119,7 +120,7 @@ class TestDownstream(unittest.TestCase): url = 'http://threebean.org' config['sync2jira']['testing'] = False - result = d.create_jira_issue(MockIssue(), config) + result = d._create_jira_issue(jira.client.JIRA(), MockIssue(), config) eq_(result, target1) client.return_value.create_issue.assert_called_with( components=[{'name': 'alsoawesome'}],