2020-11-24 15:15:51 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Atlassian
|
|
|
|
module JiraConnect
|
|
|
|
class Client < Gitlab::HTTP
|
2021-01-29 00:20:46 +05:30
|
|
|
def self.generate_update_sequence_id
|
2021-03-11 19:13:27 +05:30
|
|
|
(Time.now.utc.to_f * 1000).round
|
2021-01-29 00:20:46 +05:30
|
|
|
end
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
def initialize(base_uri, shared_secret)
|
|
|
|
@base_uri = base_uri
|
|
|
|
@shared_secret = shared_secret
|
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
def send_info(project:, update_sequence_id: nil, **args)
|
|
|
|
common = { project: project, update_sequence_id: update_sequence_id }
|
|
|
|
dev_info = args.slice(:commits, :branches, :merge_requests)
|
|
|
|
build_info = args.slice(:pipelines)
|
2021-03-08 18:12:59 +05:30
|
|
|
deploy_info = args.slice(:deployments)
|
|
|
|
ff_info = args.slice(:feature_flags)
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
responses = []
|
|
|
|
|
|
|
|
responses << store_dev_info(**common, **dev_info) if dev_info.present?
|
|
|
|
responses << store_build_info(**common, **build_info) if build_info.present?
|
2021-03-08 18:12:59 +05:30
|
|
|
responses << store_deploy_info(**common, **deploy_info) if deploy_info.present?
|
|
|
|
responses << store_ff_info(**common, **ff_info) if ff_info.present?
|
2021-02-22 17:27:13 +05:30
|
|
|
raise ArgumentError, 'Invalid arguments' if responses.empty?
|
|
|
|
|
|
|
|
responses.compact
|
|
|
|
end
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
# Fetch user information for the given account.
|
|
|
|
# https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get
|
2021-09-04 01:27:46 +05:30
|
|
|
def user_info(account_id)
|
|
|
|
r = get('/rest/api/3/user', { accountId: account_id, expand: 'groups' })
|
|
|
|
|
|
|
|
JiraUser.new(r.parsed_response) if r.code == 200
|
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
private
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
def get(path, query_params)
|
|
|
|
uri = URI.join(@base_uri, path)
|
|
|
|
uri.query = URI.encode_www_form(query_params)
|
|
|
|
|
|
|
|
self.class.get(uri, headers: headers(uri, 'GET'))
|
|
|
|
end
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
def store_ff_info(project:, feature_flags:, **opts)
|
|
|
|
items = feature_flags.map { |flag| ::Atlassian::JiraConnect::Serializers::FeatureFlagEntity.represent(flag, opts) }
|
|
|
|
items.reject! { |item| item.issue_keys.empty? }
|
|
|
|
|
|
|
|
return if items.empty?
|
|
|
|
|
|
|
|
r = post('/rest/featureflags/0.1/bulk', {
|
|
|
|
flags: items,
|
|
|
|
properties: { projectId: "project-#{project.id}" }
|
|
|
|
})
|
|
|
|
|
|
|
|
handle_response(r, 'feature flags') do |data|
|
|
|
|
failed = data['failedFeatureFlags']
|
|
|
|
if failed.present?
|
|
|
|
errors = failed.flat_map do |k, errs|
|
|
|
|
errs.map { |e| "#{k}: #{e['message']}" }
|
|
|
|
end
|
|
|
|
{ 'errorMessages' => errors }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def store_deploy_info(project:, deployments:, **opts)
|
|
|
|
items = deployments.map { |d| ::Atlassian::JiraConnect::Serializers::DeploymentEntity.represent(d, opts) }
|
|
|
|
items.reject! { |d| d.issue_keys.empty? }
|
|
|
|
|
|
|
|
return if items.empty?
|
|
|
|
|
|
|
|
r = post('/rest/deployments/0.1/bulk', { deployments: items })
|
2023-03-04 22:38:38 +05:30
|
|
|
handle_response(r, 'deployments') { |data| errors(data, 'rejectedDeployments', r) }
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
def store_build_info(project:, pipelines:, update_sequence_id: nil)
|
|
|
|
builds = pipelines.map do |pipeline|
|
2021-03-08 18:12:59 +05:30
|
|
|
build = ::Atlassian::JiraConnect::Serializers::BuildEntity.represent(
|
2021-02-22 17:27:13 +05:30
|
|
|
pipeline,
|
|
|
|
update_sequence_id: update_sequence_id
|
|
|
|
)
|
|
|
|
next if build.issue_keys.empty?
|
|
|
|
|
|
|
|
build
|
|
|
|
end.compact
|
|
|
|
return if builds.empty?
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
r = post('/rest/builds/0.1/bulk', { builds: builds })
|
2023-03-04 22:38:38 +05:30
|
|
|
handle_response(r, 'builds') { |data| errors(data, 'rejectedBuilds', r) }
|
2021-02-22 17:27:13 +05:30
|
|
|
end
|
|
|
|
|
2021-01-29 00:20:46 +05:30
|
|
|
def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil, update_sequence_id: nil)
|
2021-10-27 15:23:28 +05:30
|
|
|
repo = ::Atlassian::JiraConnect::Serializers::RepositoryEntity.represent(
|
2021-02-22 17:27:13 +05:30
|
|
|
project,
|
|
|
|
commits: commits,
|
|
|
|
branches: branches,
|
|
|
|
merge_requests: merge_requests,
|
|
|
|
user_notes_count: user_notes_count(merge_requests),
|
|
|
|
update_sequence_id: update_sequence_id
|
|
|
|
)
|
|
|
|
|
|
|
|
post('/rest/devinfo/0.10/bulk', { repositories: [repo] })
|
|
|
|
end
|
|
|
|
|
|
|
|
def post(path, payload)
|
|
|
|
uri = URI.join(@base_uri, path)
|
|
|
|
|
|
|
|
self.class.post(uri, headers: headers(uri), body: metadata.merge(payload).to_json)
|
|
|
|
end
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
def headers(uri, http_method = 'POST')
|
2021-02-22 17:27:13 +05:30
|
|
|
{
|
2021-09-04 01:27:46 +05:30
|
|
|
'Authorization' => "JWT #{jwt_token(http_method, uri)}",
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'Accept' => 'application/json'
|
2020-11-24 15:15:51 +05:30
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
def metadata
|
|
|
|
{ providerMetadata: { product: "GitLab #{Gitlab::VERSION}" } }
|
|
|
|
end
|
2020-11-24 15:15:51 +05:30
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
def handle_response(response, name, &block)
|
|
|
|
data = response.parsed_response
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
if [200, 202].include?(response.code)
|
|
|
|
yield data
|
2021-03-08 18:12:59 +05:30
|
|
|
else
|
2023-03-04 22:38:38 +05:30
|
|
|
case response.code
|
|
|
|
when 400 then { 'errorMessages' => data.map { |e| e['message'] } }
|
|
|
|
when 401 then { 'errorMessages' => ['Invalid JWT'] }
|
|
|
|
when 403 then { 'errorMessages' => ["App does not support #{name}"] }
|
|
|
|
when 413 then { 'errorMessages' => ['Data too large'] + data.map { |e| e['message'] } }
|
|
|
|
when 429 then { 'errorMessages' => ['Rate limit exceeded'] }
|
|
|
|
when 503 then { 'errorMessages' => ['Service unavailable'] }
|
|
|
|
else
|
|
|
|
{ 'errorMessages' => ['Unknown error'], 'response' => data }
|
|
|
|
end.merge('responseCode' => response.code)
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
def errors(data, key, response)
|
2021-03-08 18:12:59 +05:30
|
|
|
messages = if data[key].present?
|
|
|
|
data[key].flat_map do |rejection|
|
|
|
|
rejection['errors'].map { |e| e['message'] }
|
|
|
|
end
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
{ 'errorMessages' => messages, 'responseCode' => response.code, 'requestBody' => request_body_schema(response) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def request_body_schema(response)
|
|
|
|
Oj.load(response.request.raw_body).deep_transform_values! {}
|
|
|
|
rescue Oj::ParseError, EncodingError, Encoding::UndefinedConversionError
|
|
|
|
'Request body includes invalid JSON'
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
|
|
|
|
2021-01-29 00:20:46 +05:30
|
|
|
def user_notes_count(merge_requests)
|
|
|
|
return unless merge_requests
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
Note.count_for_collection(merge_requests.map(&:id), 'MergeRequest').to_h do |count_group|
|
2021-01-29 00:20:46 +05:30
|
|
|
[count_group.noteable_id, count_group.count]
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
2021-01-29 00:20:46 +05:30
|
|
|
end
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
def jwt_token(http_method, uri)
|
|
|
|
claims = Atlassian::Jwt.build_claims(
|
|
|
|
Atlassian::JiraConnect.app_key,
|
|
|
|
uri,
|
|
|
|
http_method,
|
|
|
|
@base_uri
|
|
|
|
)
|
|
|
|
|
|
|
|
Atlassian::Jwt.encode(claims, @shared_secret)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|