diff --git a/CHANGES.md b/CHANGES.md index b162431..2a44bfc 100755 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Change Log +## 0.9.0 + +- Add new ``jira.issues_tracker_for_apiv2`` sensor + ## 0.8.1 - Version bump to fix tagging issue, no code changes diff --git a/pack.yaml b/pack.yaml index e492cbb..9d94e33 100755 --- a/pack.yaml +++ b/pack.yaml @@ -6,6 +6,9 @@ keywords: - issues - ticket management - project management -version: 0.8.1 +version: 0.9.0 +python_versions: + - "2" + - "3" author : StackStorm, Inc. email : info@stackstorm.com diff --git a/requirements.txt b/requirements.txt index be5f41d..d25f4a6 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -jira -pyyaml -cryptography -pyjwt +jira==2.0.0 +pyyaml==3.13 +cryptography==2.4.1 +pyjwt==1.6.4 diff --git a/sensors/jira_sensor_for_apiv2.py b/sensors/jira_sensor_for_apiv2.py new file mode 100755 index 0000000..c676225 --- /dev/null +++ b/sensors/jira_sensor_for_apiv2.py @@ -0,0 +1,113 @@ +# See ./requirements.txt for requirements. +import os + +from jira.client import JIRA + +from st2reactor.sensor.base import PollingSensor + + +class JIRASensorForAPIv2(PollingSensor): + ''' + Sensor will monitor for any new projects created in JIRA and + emit trigger instance when one is created. + ''' + def __init__(self, sensor_service, config=None, poll_interval=5): + super(JIRASensorForAPIv2, self).__init__(sensor_service=sensor_service, + config=config, + poll_interval=poll_interval) + + self._jira_url = None + # The Consumer Key created while setting up the "Incoming Authentication" in + # JIRA for the Application Link. + self._consumer_key = u'' + self._rsa_key = None + self._jira_client = None + self._access_token = u'' + self._access_secret = u'' + self._projects_available = None + self._poll_interval = 30 + self._project = None + self._issues_in_project = None + self._jql_query = None + self._trigger_name = 'issues_tracker_for_apiv2' + self._trigger_pack = 'jira' + self._trigger_ref = '.'.join([self._trigger_pack, self._trigger_name]) + + def _read_cert(self, file_path): + with open(file_path) as f: + return f.read() + + def setup(self): + self._jira_url = self._config['url'] + auth_method = self._config['auth_method'] + + if auth_method == 'oauth': + rsa_cert_file = self._config['rsa_cert_file'] + if not os.path.exists(rsa_cert_file): + raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) + self._rsa_key = self._read_cert(rsa_cert_file) + self._poll_interval = self._config.get('poll_interval', self._poll_interval) + oauth_creds = { + 'access_token': self._config['oauth_token'], + 'access_token_secret': self._config['oauth_secret'], + 'consumer_key': self._config['consumer_key'], + 'key_cert': self._rsa_key + } + + self._jira_client = JIRA(options={'server': self._jira_url}, + oauth=oauth_creds) + elif auth_method == 'basic': + basic_creds = (self._config['username'], self._config['password']) + self._jira_client = JIRA(options={'server': self._jira_url}, + basic_auth=basic_creds) + + else: + msg = ('You must set auth_method to either "oauth"', + 'or "basic" your jira.yaml config file.') + raise Exception(msg) + + if self._projects_available is None: + self._projects_available = set() + for proj in self._jira_client.projects(): + self._projects_available.add(proj.key) + self._project = self._config.get('project', None) + if not self._project or self._project not in self._projects_available: + raise Exception('Invalid project (%s) to track.' % self._project) + self._jql_query = 'project=%s' % self._project + all_issues = self._jira_client.search_issues(self._jql_query, maxResults=None) + self._issues_in_project = {issue.key: issue for issue in all_issues} + + def poll(self): + self._detect_new_issues() + + def cleanup(self): + pass + + def add_trigger(self, trigger): + pass + + def update_trigger(self, trigger): + pass + + def remove_trigger(self, trigger): + pass + + def _detect_new_issues(self): + new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) + + for issue in new_issues: + if issue.key not in self._issues_in_project: + self._dispatch_issues_trigger(issue) + self._issues_in_project[issue.key] = issue + + def _dispatch_issues_trigger(self, issue): + trigger = self._trigger_ref + payload = {} + payload['project'] = self._project + payload['id'] = issue.id + payload['expand'] = issue.raw.get('expand', '') + payload['issue_key'] = issue.key + payload['issue_url'] = issue.self + payload['issue_browse_url'] = self._jira_url + '/browse/' + issue.key + payload['fields'] = issue.raw.get('fields', {}) + self._sensor_service.dispatch(trigger, payload) diff --git a/sensors/jira_sensor_for_apiv2.yaml b/sensors/jira_sensor_for_apiv2.yaml new file mode 100644 index 0000000..cca348f --- /dev/null +++ b/sensors/jira_sensor_for_apiv2.yaml @@ -0,0 +1,26 @@ +--- + class_name: "JIRASensorForAPIv2" + entry_point: "jira_sensor_for_apiv2.py" + description: "Sensor which monitors JIRA for new tickets" + poll_interval: 30 + trigger_types: + - + name: "issues_tracker_for_apiv2" + description: "Trigger which indicates that a new issue has been created" + payload_schema: + type: "object" + properties: + project: + type: "string" + id: + type: "string" + expand: + type: "string" + issue_key: + type: "string" + issue_url: + type: "string" + issue_browse_url: + type: "string" + fields: + type: "object" diff --git a/tests/test_sensor_jira_sensor_for_apiv2.py b/tests/test_sensor_jira_sensor_for_apiv2.py new file mode 100644 index 0000000..49e55df --- /dev/null +++ b/tests/test_sensor_jira_sensor_for_apiv2.py @@ -0,0 +1,93 @@ +import mock + +from st2tests.base import BaseSensorTestCase + +from jira_sensor_for_apiv2 import JIRASensorForAPIv2 + +JIRA_URL = "http://jira.hoge.com/" +PROJECT_NAME = "PROJECT" +ISSUE_ID = "112" +ISSUE_SELF = "http://jira.hoge.com/rest/api/2/issue/" + ISSUE_ID +ISSUE_KEY = "ISSUEKEY-1" + +ISSUE = { + "project": PROJECT_NAME, + "id": ISSUE_ID, + "expand": "html,editmeta,changelog", + "fields": { + "assignee": None, + "creator": { + "displayName": "user01@test", + "name": "user-name" + }, + "issuetype": { + "name": "task" + }, + "components": [ + { + "id": "421203", + "name": "test_components" + } + ] + } +} + +MOCK_ISSUE_RAW = ISSUE.copy() +MOCK_ISSUE_RAW["key"] = ISSUE_KEY +MOCK_ISSUE_RAW["self"] = ISSUE_SELF + +PAYLOAD = ISSUE.copy() +PAYLOAD["issue_key"] = ISSUE_KEY +PAYLOAD["issue_url"] = ISSUE_SELF +PAYLOAD["issue_browse_url"] = JIRA_URL + '/browse/' + ISSUE_KEY + +TRIGGER = { + "trace_context": None, + "trigger": "jira.issues_tracker_for_apiv2", + "payload": PAYLOAD +} + + +class JIRASensorForAPIv2TestCase(BaseSensorTestCase): + maxDiff = None + sensor_cls = JIRASensorForAPIv2 + + def test_poll(self): + sensor = self.get_sensor_instance() + sensor._jira_client = mock.Mock() + sensor._jira_client.search_issues.return_value = [] + sensor._issues_in_project = {} + + # no issues + sensor.poll() + self.assertEqual(self.get_dispatched_triggers(), []) + + # 1 new issue + issue = mock.Mock() + issue.raw = MOCK_ISSUE_RAW + issue.id = ISSUE_ID + issue.self = ISSUE_SELF + issue.key = ISSUE_KEY + + sensor._project = PROJECT_NAME + sensor._jira_url = JIRA_URL + sensor._jira_client.search_issues.return_value = [issue] + + sensor.poll() + payload = self.get_dispatched_triggers()[0]['payload'] + self.assertEqual(payload, PAYLOAD) + + # 1 new issue + issue = mock.Mock() + issue.raw = MOCK_ISSUE_RAW + issue.id = ISSUE_ID + issue.self = ISSUE_SELF + issue.key = ISSUE_KEY + + sensor._project = PROJECT_NAME + sensor._jira_url = JIRA_URL + sensor._jira_client.search_issues.return_value = [issue] + + sensor.poll() + payload = self.get_dispatched_triggers()[0]['payload'] + self.assertEqual(payload, PAYLOAD)