jira_agent.rb 4.9 KB

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