jira_agent.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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, "you need to specify password if user name is set") if options['username'].present? and not options['password'].present?
  44. errors.add(:base, "you need to specify your jira URL") unless options['jira_url'].present?
  45. errors.add(:base, "you need to specify the expected update period") unless options['expected_update_period_in_days'].present?
  46. errors.add(:base, "you need to specify request timeout") unless options['timeout'].present?
  47. end
  48. def working?
  49. event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
  50. end
  51. def check
  52. last_run = nil
  53. current_run = Time.now.utc.iso8601
  54. last_run = Time.parse(memory[:last_run]) if memory[:last_run]
  55. issues = get_issues(last_run)
  56. issues.each do |issue|
  57. updated = Time.parse(issue['fields']['updated'])
  58. # this check is more precise than in get_issues()
  59. # see get_issues() for explanation
  60. if not last_run or updated > last_run
  61. create_event :payload => issue
  62. end
  63. end
  64. memory[:last_run] = current_run
  65. end
  66. private
  67. def request_url(jql, start_at)
  68. "#{interpolated[:jira_url]}/rest/api/2/search?jql=#{CGI::escape(jql)}&fields=*all&startAt=#{start_at}"
  69. end
  70. def request_options
  71. ropts = { headers: {"User-Agent" => user_agent} }
  72. if !interpolated[:username].empty?
  73. ropts = ropts.merge({:basic_auth => {:username =>interpolated[:username], :password=>interpolated[:password]}})
  74. end
  75. ropts
  76. end
  77. def get(url, options)
  78. response = HTTParty.get(url, options)
  79. if response.code == 400
  80. raise RuntimeError.new("Jira error: #{response['errorMessages']}")
  81. elsif response.code == 403
  82. raise RuntimeError.new("Authentication failed: Forbidden (403)")
  83. elsif response.code != 200
  84. raise RuntimeError.new("Request failed: #{response}")
  85. end
  86. response
  87. end
  88. def get_issues(since)
  89. startAt = 0
  90. issues = []
  91. # JQL doesn't have an ability to specify timezones
  92. # Because of this we have to fetch issues 24 h
  93. # earlier and filter out unnecessary ones at a later
  94. # stage. Fortunately, the 'updated' field has GMT
  95. # offset
  96. since -= 24*60*60 if since
  97. jql = ""
  98. if !interpolated[:jql].empty? && since
  99. jql = "(#{interpolated[:jql]}) and updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'"
  100. else
  101. jql = interpolated[:jql] if !interpolated[:jql].empty?
  102. jql = "updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" if since
  103. end
  104. start_time = Time.now
  105. request_limit = 0
  106. loop do
  107. response = get(request_url(jql, startAt), request_options)
  108. if response['issues'].length == 0
  109. request_limit+=1
  110. end
  111. if request_limit > MAX_EMPTY_REQUESTS
  112. raise RuntimeError.new("There is no progress while fetching issues")
  113. end
  114. if Time.now > start_time + interpolated['timeout'].to_i * 60
  115. raise RuntimeError.new("Timeout exceeded while fetching issues")
  116. end
  117. issues += response['issues']
  118. startAt += response['issues'].length
  119. break if startAt >= response['total']
  120. end
  121. issues
  122. end
  123. end
  124. end