jira_agent.rb 4.9 KB

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