123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- #!/usr/bin/env ruby
- require 'cgi'
- require 'httparty'
- require 'date'
- module Agents
- class JiraAgent < Agent
- include WebRequestConcern
- cannot_receive_events!
- description <<~MD
- The Jira Agent subscribes to Jira issue updates.
- - `jira_url` specifies the full URL of the jira installation, including https://
- - `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.#{' '}
- - `username` and `password` are optional, and may need to be specified if your Jira instance is read-protected
- - `timeout` is an optional parameter that specifies how long the request processing may take in minutes.
- The agent does periodic queries and emits the events containing the updated issues in JSON format.
- 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.
- MD
- event_description <<~MD
- Events are the raw JSON generated by Jira REST API
- {
- "expand": "editmeta,renderedFields,transitions,changelog,operations",
- "id": "80127",
- "self": "https://jira.atlassian.com/rest/api/2/issue/80127",
- "key": "BAM-3512",
- "fields": {
- ...
- }
- }
- MD
- default_schedule "every_10m"
- MAX_EMPTY_REQUESTS = 10
- def default_options
- {
- 'username' => '',
- 'password' => '',
- 'jira_url' => 'https://jira.atlassian.com',
- 'jql' => '',
- 'expected_update_period_in_days' => '7',
- 'timeout' => '1'
- }
- end
- def validate_options
- errors.add(:base,
- "you need to specify password if user name is set") if options['username'].present? and !options['password'].present?
- errors.add(:base, "you need to specify your jira URL") unless options['jira_url'].present?
- errors.add(:base,
- "you need to specify the expected update period") unless options['expected_update_period_in_days'].present?
- errors.add(:base, "you need to specify request timeout") unless options['timeout'].present?
- end
- def working?
- event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
- end
- def check
- last_run = nil
- current_run = Time.now.utc.iso8601
- last_run = Time.parse(memory[:last_run]) if memory[:last_run]
- issues = get_issues(last_run)
- issues.each do |issue|
- updated = Time.parse(issue['fields']['updated'])
- # this check is more precise than in get_issues()
- # see get_issues() for explanation
- if !last_run or updated > last_run
- create_event payload: issue
- end
- end
- memory[:last_run] = current_run
- end
- private
- def request_url(jql, start_at)
- "#{interpolated[:jira_url]}/rest/api/2/search?jql=#{CGI.escape(jql)}&fields=*all&startAt=#{start_at}"
- end
- def request_options
- ropts = { headers: { "User-Agent" => user_agent } }
- if !interpolated[:username].empty?
- ropts = ropts.merge({
- basic_auth: {
- username: interpolated[:username],
- password: interpolated[:password]
- }
- })
- end
- ropts
- end
- def get(url, options)
- response = HTTParty.get(url, options)
- case response.code
- when 200
- # OK
- when 400
- raise "Jira error: #{response['errorMessages']}"
- when 403
- raise "Authentication failed: Forbidden (403)"
- else
- raise "Request failed: #{response}"
- end
- response
- end
- def get_issues(since)
- startAt = 0
- issues = []
- # JQL doesn't have an ability to specify timezones
- # Because of this we have to fetch issues 24 h
- # earlier and filter out unnecessary ones at a later
- # stage. Fortunately, the 'updated' field has GMT
- # offset
- since -= 24 * 60 * 60 if since
- jql = ""
- if !interpolated[:jql].empty? && since
- jql = "(#{interpolated[:jql]}) and updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'"
- else
- jql = interpolated[:jql] if !interpolated[:jql].empty?
- jql = "updated >= '#{since.strftime('%Y-%m-%d %H:%M')}'" if since
- end
- start_time = Time.now
- request_limit = 0
- loop do
- response = get(request_url(jql, startAt), request_options)
- if response['issues'].length == 0
- request_limit += 1
- end
- if request_limit > MAX_EMPTY_REQUESTS
- raise "There is no progress while fetching issues"
- end
- if Time.now > start_time + interpolated['timeout'].to_i * 60
- raise "Timeout exceeded while fetching issues"
- end
- issues += response['issues']
- startAt += response['issues'].length
- break if startAt >= response['total']
- end
- issues
- end
- end
- end
|