Merge pull request #313 from racktear/jira_agent

Add JiraAgent

Andrew Cantino 10 年之前
父節點
當前提交
3633b26778
共有 2 個文件被更改,包括 160 次插入0 次删除
  1. 160 0
      app/models/agents/jira_agent.rb
  2. 0 0
      spec/data_fixtures/jira.json

+ 160 - 0
app/models/agents/jira_agent.rb

@@ -0,0 +1,160 @@
1
+#!/usr/bin/env ruby
2
+
3
+require 'cgi'
4
+require 'httparty'
5
+require 'date'
6
+
7
+module Agents
8
+  class JiraAgent < Agent
9
+    cannot_receive_events!
10
+
11
+    description <<-MD
12
+      The Jira Agent subscribes to Jira issue updates.
13
+
14
+      `jira_url` specifies the full URL of the jira installation, including https://
15
+      `jql` is an optional Jira Query Language-based filter to limit the flow of events. See [JQL Docs](https://confluence.atlassian.com/display/JIRA/Advanced+Searching) for details. 
16
+      `username` and `password` are optional, and may need to be specified if your Jira instance is read-protected
17
+      `timeout` is an optional parameter that specifies how long the request processing may take in minutes.
18
+
19
+      The agent does periodic queries and emits the events containing the updated issues in JSON format.
20
+      NOTE: upon the first execution, the agent will fetch everything available by the JQL query. So if it's not desirable, limit the `jql` query by date.
21
+    MD
22
+
23
+    event_description <<-MD
24
+      Events are the raw JSON generated by Jira REST API
25
+
26
+      {
27
+        "expand": "editmeta,renderedFields,transitions,changelog,operations",
28
+        "id": "80127",
29
+        "self": "https://jira.atlassian.com/rest/api/2/issue/80127",
30
+        "key": "BAM-3512",
31
+        "fields": {
32
+          ...
33
+        }
34
+      }
35
+    MD
36
+
37
+    default_schedule "every_10m"
38
+    MAX_EMPTY_REQUESTS = 10
39
+
40
+    def default_options
41
+      {
42
+        'username'  => '',
43
+        'password' => '',
44
+        'jira_url' => 'https://jira.atlassian.com',
45
+        'jql' => '',
46
+        'expected_update_period_in_days' => '7',
47
+        'timeout' => '1'
48
+      }
49
+    end
50
+
51
+    def validate_options
52
+      errors.add(:base, "you need to specify password if user name is set") if options['username'].present? and not options['password'].present?
53
+      errors.add(:base, "you need to specify your jira URL") unless options['jira_url'].present?
54
+      errors.add(:base, "you need to specify the expected update period") unless options['expected_update_period_in_days'].present?
55
+      errors.add(:base, "you need to specify request timeout") unless options['timeout'].present?
56
+    end
57
+
58
+    def working?
59
+      event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs?
60
+    end
61
+
62
+    def check
63
+      last_run = nil
64
+
65
+      current_run = Time.now.utc.iso8601
66
+      last_run = Time.parse(memory[:last_run]) if memory[:last_run]
67
+      issues = get_issues(last_run)
68
+
69
+      issues.each do |issue|
70
+        updated = Time.parse(issue['fields']['updated'])
71
+
72
+        # this check is more precise than in get_issues()
73
+        # see get_issues() for explanation
74
+        if not last_run or updated > last_run
75
+          create_event :payload => issue
76
+        end
77
+      end
78
+
79
+      memory[:last_run] = current_run
80
+    end
81
+
82
+  private
83
+    def request_url(jql, start_at)
84
+      "#{options[:jira_url]}/rest/api/2/search?jql=#{CGI::escape(jql)}&fields=*all&startAt=#{start_at}"
85
+    end
86
+
87
+    def request_options
88
+      ropts = {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
89
+
90
+      if !options[:username].empty?
91
+        ropts = ropts.merge({:basic_auth => {:username =>options[:username], :password=>options[:password]}})
92
+      end
93
+
94
+      ropts
95
+    end
96
+
97
+    def get(url, options)
98
+        response = HTTParty.get(url, options)
99
+
100
+        if response.code == 400
101
+          raise RuntimeError.new("Jira error: #{response['errorMessages']}") 
102
+        elsif response.code == 403
103
+          raise RuntimeError.new("Authentication failed: Forbidden (403)")
104
+        elsif response.code != 200
105
+          raise RuntimeError.new("Request failed: #{response}")
106
+        end
107
+
108
+        response
109
+    end
110
+
111
+    def get_issues(since)
112
+      startAt = 0
113
+      issues = []
114
+
115
+      # JQL doesn't have an ability to specify timezones
116
+      # Because of this we have to fetch issues 24 h
117
+      # earlier and filter out unnecessary ones at a later
118
+      # stage. Fortunately, the 'updated' field has GMT
119
+      # offset
120
+      since -= 24*60*60 if since
121
+
122
+      jql = ""
123
+
124
+      if !options[:jql].empty? && since
125
+        jql = "(#{options[:jql]}) and updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'"
126
+      else
127
+        jql = options[:jql] if !options[:jql].empty?
128
+        jql = "updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" if since
129
+      end
130
+
131
+      start_time = Time.now
132
+
133
+      request_limit = 0
134
+      loop do
135
+        response = get(request_url(jql, startAt), request_options)
136
+
137
+        if response['issues'].length == 0
138
+          request_limit+=1
139
+        end
140
+
141
+        if request_limit > MAX_EMPTY_REQUESTS
142
+          raise RuntimeError.new("There is no progress while fetching issues")
143
+        end
144
+
145
+        if Time.now > start_time + options['timeout'].to_i * 60
146
+          raise RuntimeError.new("Timeout exceeded while fetching issues")
147
+        end
148
+
149
+        issues += response['issues']
150
+        startAt += response['issues'].length
151
+ 
152
+        break if startAt >= response['total']
153
+      end
154
+
155
+      issues
156
+    end
157
+
158
+  end
159
+end
160
+

+ 0 - 0
spec/data_fixtures/jira.json

@@ -0,0 +1 @@