Commit 412f8f47 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'cernvcs/gitlab-ee-feature/jira_api_path'

parents d0c45920 f4501143
...@@ -2,6 +2,7 @@ v 8.2.0 ...@@ -2,6 +2,7 @@ v 8.2.0
- Invalidate stored jira password if the endpoint URL is changed - Invalidate stored jira password if the endpoint URL is changed
- Fix: Page is not reloaded periodically to check if rebase is finished - Fix: Page is not reloaded periodically to check if rebase is finished
- When someone as marked as a required approver for a merge request, an email should be sent - When someone as marked as a required approver for a merge request, an email should be sent
- Allow configuring the Jira API path (Alex Lossent)
v 8.1.0 (unreleased) v 8.1.0 (unreleased)
- added an issues template (Hannes Rosenögger) - added an issues template (Hannes Rosenögger)
......
class Projects::ServicesController < Projects::ApplicationController class Projects::ServicesController < Projects::ApplicationController
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain, ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook, :room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type, :build_key, :server, :teamcity_url, :drone_url, :build_type,
......
...@@ -22,15 +22,18 @@ class JiraService < IssueTrackerService ...@@ -22,15 +22,18 @@ class JiraService < IssueTrackerService
include HTTParty include HTTParty
include Gitlab::Application.routes.url_helpers include Gitlab::Application.routes.url_helpers
prop_accessor :username, :password, :api_version, :jira_issue_transition_id, DEFAULT_API_VERSION = 2
prop_accessor :username, :password, :api_url, :jira_issue_transition_id,
:title, :description, :project_url, :issues_url, :new_issue_url :title, :description, :project_url, :issues_url, :new_issue_url
before_validation :set_api_version, :set_jira_issue_transition_id before_validation :set_api_url, :set_jira_issue_transition_id
before_update :reset_password before_update :reset_password
def reset_password def reset_password
if project_url_changed? && !password_touched? # don't reset the password if a new one is provided
if api_url_changed? && !password_touched?
self.password = nil self.password = nil
end end
end end
...@@ -69,9 +72,9 @@ class JiraService < IssueTrackerService ...@@ -69,9 +72,9 @@ class JiraService < IssueTrackerService
def fields def fields
super.push( super.push(
{ type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' },
{ type: 'text', name: 'username', placeholder: '' }, { type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'api_version', placeholder: '2' },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
) )
end end
...@@ -118,7 +121,7 @@ class JiraService < IssueTrackerService ...@@ -118,7 +121,7 @@ class JiraService < IssueTrackerService
def test_settings def test_settings
result = JiraService.get( result = JiraService.get(
jira_project_url, jira_api_test_url,
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}" 'Authorization' => "Basic #{auth}"
...@@ -127,22 +130,31 @@ class JiraService < IssueTrackerService ...@@ -127,22 +130,31 @@ class JiraService < IssueTrackerService
case result.code case result.code
when 201, 200 when 201, 200
Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Sucessfully connected to #{server_url}.") Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Sucessfully connected to #{api_url}.")
true true
else else
Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}")
false false
end end
rescue Errno::ECONNREFUSED => e rescue Errno::ECONNREFUSED => e
Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{server_url}." Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}."
false false
end end
private private
def build_api_url_from_project_url
server = URI(project_url)
default_ports = [["http",80],["https",443]].include?([server.scheme,server.port])
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
"#{server_url}/rest/api/#{DEFAULT_API_VERSION}"
rescue
"" # looks like project URL was not valid
end
def set_api_version def set_api_url
self.api_version ||= "2" self.api_url = build_api_url_from_project_url if self.api_url.blank?
end end
def set_jira_issue_transition_id def set_jira_issue_transition_id
...@@ -241,14 +253,6 @@ class JiraService < IssueTrackerService ...@@ -241,14 +253,6 @@ class JiraService < IssueTrackerService
false false
end end
def server_url
server = URI(project_url)
default_ports = [80, 443].include?(server.port)
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
server_url
end
def resource_url(resource) def resource_url(resource)
"#{Settings.gitlab['url'].chomp("/")}#{resource}" "#{Settings.gitlab['url'].chomp("/")}#{resource}"
end end
...@@ -268,14 +272,14 @@ class JiraService < IssueTrackerService ...@@ -268,14 +272,14 @@ class JiraService < IssueTrackerService
end end
def close_issue_url(issue_name) def close_issue_url(issue_name)
"#{server_url}/rest/api/#{self.api_version}/issue/#{issue_name}/transitions" "#{self.api_url}/issue/#{issue_name}/transitions"
end end
def comment_url(issue_name) def comment_url(issue_name)
"#{server_url}/rest/api/#{self.api_version}/issue/#{issue_name}/comment" "#{self.api_url}/issue/#{issue_name}/comment"
end end
def jira_project_url def jira_api_test_url
"#{server_url}/rest/api/#{self.api_version}/project" "#{self.api_url}/myself"
end end
end end
class SetJiraServiceApiUrl < ActiveRecord::Migration
# This migration can be performed online without errors, but some Jira API calls may be missed
# when doing so because api_url is not yet available.
def build_api_url_from_project_url(project_url, api_version)
# this is the exact logic previously used to build the Jira API URL from project_url
server = URI(project_url)
default_ports = [80, 443].include?(server.port)
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
"#{server_url}/rest/api/#{api_version}"
end
def get_api_version_from_api_url(api_url)
match = /\/rest\/api\/(?<api_version>\w+)$/.match(api_url)
match && match['api_version']
end
def change
reversible do |dir|
select_all("SELECT id, properties FROM services WHERE services.type IN ('JiraService')").each do |jira_service|
id = jira_service["id"]
properties = JSON.parse(jira_service["properties"])
properties_was = properties.clone
dir.up do
# remove api_version and set api_url
if properties['api_version'].present? && properties['project_url'].present?
begin
properties['api_url'] ||= build_api_url_from_project_url(properties['project_url'], properties['api_version'])
rescue
# looks like project_url was not a valid URL. Do nothing.
end
end
properties.delete('api_version') if properties.include?('api_version')
end
dir.down do
# remove api_url and set api_version (default to '2')
properties['api_version'] ||= get_api_version_from_api_url(properties['api_url']) || '2'
properties.delete('api_url') if properties.include?('api_url')
end
if properties != properties_was
execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
end
end
end
end
end
...@@ -74,9 +74,9 @@ Fill in the required details on the page: ...@@ -74,9 +74,9 @@ Fill in the required details on the page:
* `project url` The URL to the JIRA project which is being linked to this GitLab project. * `project url` The URL to the JIRA project which is being linked to this GitLab project.
* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project. * `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project.
* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project. * `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project.
* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`.
* `username` The username of the user created in [configuring JIRA step](#configuring-jira). * `username` The username of the user created in [configuring JIRA step](#configuring-jira).
* `password` The password of the user created in [configuring JIRA step](#configuring-jira). * `password` The password of the user created in [configuring JIRA step](#configuring-jira).
* `api version` The version of the JIRA API. By default, version `2` is used.
* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well) * `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well)
After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. After saving the configuration, your GitLab project will be able to interact with the linked JIRA project.
......
...@@ -191,14 +191,14 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -191,14 +191,14 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
fill_in 'Project url', with: 'http://jira.example' fill_in 'Project url', with: 'http://jira.example'
fill_in 'Username', with: 'gitlab' fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab' fill_in 'Password', with: 'gitlab'
fill_in 'Api version', with: '2' fill_in 'Api url', with: 'http://jira.example/rest/api/2'
click_button 'Save' click_button 'Save'
end end
step 'I should see jira service settings saved' do step 'I should see jira service settings saved' do
expect(find_field('Project url').value).to eq 'http://jira.example' expect(find_field('Project url').value).to eq 'http://jira.example'
expect(find_field('Username').value).to eq 'gitlab' expect(find_field('Username').value).to eq 'gitlab'
expect(find_field('Api version').value).to eq '2' expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2'
end end
step 'I click Atlassian Bamboo CI service link' do step 'I click Atlassian Bamboo CI service link' do
......
...@@ -40,9 +40,9 @@ describe JiraService do ...@@ -40,9 +40,9 @@ describe JiraService do
service_hook: true, service_hook: true,
project_url: 'http://jira.example.com', project_url: 'http://jira.example.com',
username: 'gitlab_jira_username', username: 'gitlab_jira_username',
password: 'gitlab_jira_password', password: 'gitlab_jira_password'
api_version: '2'
) )
@jira_service.save # will build API URL, as api_url was not specified above
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user) @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication # https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
...@@ -66,6 +66,72 @@ describe JiraService do ...@@ -66,6 +66,72 @@ describe JiraService do
end end
end end
describe "Stored password invalidation" do
let(:project) { create(:project) }
context "when a password was previously set" do
before do
@jira_service = JiraService.create(
project: create(:project),
properties: {
api_url: 'http://jira.example.com/rest/api/2',
username: 'mic',
password: "password"
}
)
end
it "reset password if url changed" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
it "does not reset password if username changed" do
@jira_service.username = "some_name"
@jira_service.save
expect(@jira_service.password).to eq("password")
end
it "does not reset password if new url is set together with password, even if it's the same password" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
it "should reset password if url changed, even if setter called multiple times" do
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
end
context "when no password was previously set" do
before do
@jira_service = JiraService.create(
project: create(:project),
properties: {
api_url: 'http://jira.example.com/rest/api/2',
username: 'mic'
}
)
end
it "saves password if new url is set together with password" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
end
end
describe "Validations" do describe "Validations" do
context "active" do context "active" do
before do before do
...@@ -140,69 +206,4 @@ describe JiraService do ...@@ -140,69 +206,4 @@ describe JiraService do
end end
end end
end end
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
context "when a password was previously set" do
before do
@service = JiraService.create(
project: create(:project),
properties: {
project_url: 'http://gitlab.com',
username: 'mic',
password: "password"
}
)
end
it "reset password if url changed" do
@service.project_url = 'http://gitlab1.com'
@service.save
expect(@service.password).to be_nil
end
it "does not reset password if username changed" do
@service.username = "some_name"
@service.save
expect(@service.password).to eq("password")
end
it "does not reset password if new url is set together with password, even if it's the same password" do
@service.project_url = 'http://gitlab_edited.com'
@service.password = 'password'
@service.save
expect(@service.password).to eq("password")
expect(@service.project_url).to eq("http://gitlab_edited.com")
end
it "should reset password if url changed, even if setter called multiple times" do
@service.project_url = 'http://gitlab1.com'
@service.project_url = 'http://gitlab1.com'
@service.save
expect(@service.password).to be_nil
end
end
context "when no password was previously set" do
before do
@service = JiraService.create(
project: create(:project),
properties: {
project_url: 'http://gitlab.com',
username: 'mic'
}
)
end
it "saves password if new url is set together with password" do
@service.project_url = 'http://gitlab_edited.com'
@service.password = 'password'
@service.save
expect(@service.password).to eq("password")
expect(@service.project_url).to eq("http://gitlab_edited.com")
end
end
end
end end
...@@ -278,7 +278,7 @@ describe GitPushService do ...@@ -278,7 +278,7 @@ describe GitPushService do
WebMock.stub_request(:post, jira_api_transition_url) WebMock.stub_request(:post, jira_api_transition_url)
WebMock.stub_request(:post, jira_api_comment_url) WebMock.stub_request(:post, jira_api_comment_url)
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
WebMock.stub_request(:get, jira_api_project_url) WebMock.stub_request(:get, jira_api_test_url)
allow(closing_commit).to receive_messages({ allow(closing_commit).to receive_messages({
issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
......
...@@ -5,7 +5,8 @@ module JiraServiceHelper ...@@ -5,7 +5,8 @@ module JiraServiceHelper
"title"=>"JIRA tracker", "title"=>"JIRA tracker",
"project_url"=>"http://jira.example/issues/?jql=project=A", "project_url"=>"http://jira.example/issues/?jql=project=A",
"issues_url"=>"http://jira.example/browse/JIRA-1", "issues_url"=>"http://jira.example/browse/JIRA-1",
"new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa" "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa",
"api_url"=>"http://jira.example/rest/api/2"
} }
jira_tracker.update_attributes(properties: properties, active: true) jira_tracker.update_attributes(properties: properties, active: true)
...@@ -60,7 +61,7 @@ module JiraServiceHelper ...@@ -60,7 +61,7 @@ module JiraServiceHelper
'http://jira.example/rest/api/2/issue/JIRA-1/transitions' 'http://jira.example/rest/api/2/issue/JIRA-1/transitions'
end end
def jira_api_project_url def jira_api_test_url
'http://jira.example/rest/api/2/project' 'http://jira.example/rest/api/2/myself'
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment