Commit c5cb85ca authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Kerri Miller

Move CI integrations to Integrations namespace

This change moves the CI integrations to the `Integrations::` namespace
as part of https://gitlab.com/gitlab-org/gitlab/-/issues/201855.
parent 71302e5f
...@@ -1649,19 +1649,14 @@ Gitlab/NamespacedClass: ...@@ -1649,19 +1649,14 @@ Gitlab/NamespacedClass:
- 'app/models/project_repository_storage_move.rb' - 'app/models/project_repository_storage_move.rb'
- 'app/models/project_services/alerts_service.rb' - 'app/models/project_services/alerts_service.rb'
- 'app/models/project_services/alerts_service_data.rb' - 'app/models/project_services/alerts_service_data.rb'
- 'app/models/project_services/buildkite_service.rb'
- 'app/models/project_services/chat_notification_service.rb' - 'app/models/project_services/chat_notification_service.rb'
- 'app/models/project_services/ci_service.rb'
- 'app/models/project_services/discord_service.rb' - 'app/models/project_services/discord_service.rb'
- 'app/models/project_services/drone_ci_service.rb'
- 'app/models/project_services/hangouts_chat_service.rb' - 'app/models/project_services/hangouts_chat_service.rb'
- 'app/models/project_services/issue_tracker_data.rb' - 'app/models/project_services/issue_tracker_data.rb'
- 'app/models/project_services/jenkins_service.rb'
- 'app/models/project_services/jira_tracker_data.rb' - 'app/models/project_services/jira_tracker_data.rb'
- 'app/models/project_services/mattermost_service.rb' - 'app/models/project_services/mattermost_service.rb'
- 'app/models/project_services/mattermost_slash_commands_service.rb' - 'app/models/project_services/mattermost_slash_commands_service.rb'
- 'app/models/project_services/microsoft_teams_service.rb' - 'app/models/project_services/microsoft_teams_service.rb'
- 'app/models/project_services/mock_ci_service.rb'
- 'app/models/project_services/mock_monitoring_service.rb' - 'app/models/project_services/mock_monitoring_service.rb'
- 'app/models/project_services/monitoring_service.rb' - 'app/models/project_services/monitoring_service.rb'
- 'app/models/project_services/open_project_tracker_data.rb' - 'app/models/project_services/open_project_tracker_data.rb'
...@@ -1670,7 +1665,6 @@ Gitlab/NamespacedClass: ...@@ -1670,7 +1665,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_services/slack_service.rb' - 'app/models/project_services/slack_service.rb'
- 'app/models/project_services/slack_slash_commands_service.rb' - 'app/models/project_services/slack_slash_commands_service.rb'
- 'app/models/project_services/slash_commands_service.rb' - 'app/models/project_services/slash_commands_service.rb'
- 'app/models/project_services/teamcity_service.rb'
- 'app/models/project_services/unify_circuit_service.rb' - 'app/models/project_services/unify_circuit_service.rb'
- 'app/models/project_services/webex_teams_service.rb' - 'app/models/project_services/webex_teams_service.rb'
- 'app/models/project_setting.rb' - 'app/models/project_setting.rb'
......
# frozen_string_literal: true # frozen_string_literal: true
# This concern is used by registerd services such as TeamCityService and # This concern is used by registered integrations such as Integrations::TeamCity and
# DroneCiService and add methods to perform validations on the received # Integrations::DroneCi and adds methods to perform validations on the received
# data. # data.
module ServicePushDataValidations module ServicePushDataValidations
......
# frozen_string_literal: true # frozen_string_literal: true
module Integrations module Integrations
class Bamboo < CiService class Bamboo < BaseCi
include ActionView::Helpers::UrlHelper include ActionView::Helpers::UrlHelper
include ReactiveService include ReactiveService
......
# frozen_string_literal: true
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab merge requests
module Integrations
class BaseCi < Integration
default_value_for :category, 'ci'
def valid_token?(token)
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
end
def self.supported_events
%w(push)
end
# Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
# implement inside child
end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
#
# Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
# @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
def commit_status(sha, ref)
# implement inside child
end
end
end
# frozen_string_literal: true
require "addressable/uri"
module Integrations
class Buildkite < BaseCi
include ReactiveService
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
validates :project_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def self.supported_events
%w(push merge_request tag_push)
end
# This is a stub method to work with deprecated API response
# TODO: remove enable_ssl_verification after 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/222808
def enable_ssl_verification
true
end
# Since SSL verification will always be enabled for Buildkite,
# we no longer needs to store the boolean.
# This is a stub method to work with deprecated API param.
# TODO: remove enable_ssl_verification after 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/222808
def enable_ssl_verification=(_value)
self.properties.delete('enable_ssl_verification') # Remove unused key
end
def webhook_url
"#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
hook.enable_ssl_verification = true
hook.save
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data)
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def commit_status_path(sha)
"#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
end
def build_page(sha, ref)
"#{project_url}/builds?commit=#{sha}"
end
def title
'Buildkite'
end
def description
'Run CI/CD pipelines with Buildkite.'
end
def self.to_param
'buildkite'
end
def fields
[
{ type: 'text',
name: 'token',
title: 'Integration Token',
help: 'This token will be provided when you create a Buildkite pipeline with a GitLab repository',
required: true },
{ type: 'text',
name: 'project_url',
title: 'Pipeline URL',
placeholder: "#{ENDPOINT}/acme-inc/test-pipeline",
required: true }
]
end
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
status =
if response&.code == 200 && response['status']
response['status']
else
:error
end
{ commit_status: status }
end
private
def webhook_token
token_parts.first
end
def status_token
token_parts.second
end
def token_parts
if token.present?
token.split(':')
else
[]
end
end
def buildkite_endpoint(subdomain = nil)
if subdomain.present?
uri = Addressable::URI.parse(ENDPOINT)
new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
if uri.port.present?
"#{new_endpoint}:#{uri.port}"
else
new_endpoint
end
else
ENDPOINT
end
end
def request_options
{ verify: false, extra_log_info: { project_id: project_id } }
end
end
end
# frozen_string_literal: true
module Integrations
class DroneCi < BaseCi
include ReactiveService
include ServicePushDataValidations
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
# If using a service template, project may not be available
hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
def execute(data)
case data[:object_kind]
when 'push'
service_hook.execute(data) if push_valid?(data)
when 'merge_request'
service_hook.execute(data) if merge_request_valid?(data)
when 'tag_push'
service_hook.execute(data) if tag_push_valid?(data)
end
end
def allow_target_ci?
true
end
def self.supported_events
%w(push merge_request tag_push)
end
def commit_status_path(sha, ref)
Gitlab::Utils.append_path(
drone_url,
"gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) { |cached| cached[:commit_status] }
end
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
verify: enable_ssl_verification,
extra_log_info: { project_id: project_id })
status =
if response && response.code == 200 && response['status']
case response['status']
when 'killed'
:canceled
when 'failure', 'error'
# Because drone return error if some test env failed
:failed
else
response["status"]
end
else
:error
end
{ commit_status: status }
end
def build_page(sha, ref)
Gitlab::Utils.append_path(
drone_url,
"gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
def title
'Drone'
end
def description
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
def self.to_param
'drone_ci'
end
def help
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
def fields
[
{ type: 'text', name: 'token', help: s_('ProjectService|Token for the Drone project.'), required: true },
{ type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
end
end
# frozen_string_literal: true
module Integrations
class Jenkins < BaseCi
include ActionView::Helpers::UrlHelper
prop_accessor :jenkins_url, :project_name, :username, :password
before_update :reset_password
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
default_value_for :push_events, true
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
after_save :compose_service_hook, if: :activated?
def reset_password
# don't reset the password if a new one is provided
if (jenkins_url_changed? || username.blank?) && !password_touched?
self.password = nil
end
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = hook_url
hook.save
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data, "#{data[:object_kind]}_hook")
end
def test(data)
begin
result = execute(data)
return { success: false, result: result[:message] } if result[:http_status] != 200
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result[:message] }
end
def hook_url
url = URI.parse(jenkins_url)
url.path = File.join(url.path || '/', "project/#{project_name}")
url.user = ERB::Util.url_encode(username) unless username.blank?
url.password = ERB::Util.url_encode(password) unless password.blank?
url.to_s
end
def self.supported_events
%w(push merge_request tag_push)
end
def title
'Jenkins'
end
def description
s_('Run CI/CD pipelines with Jenkins.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'jenkins'
end
def fields
[
{
type: 'text',
name: 'jenkins_url',
title: s_('ProjectService|Jenkins server URL'),
required: true,
placeholder: 'http://jenkins.example.com',
help: s_('The URL of the Jenkins server.')
},
{
type: 'text',
name: 'project_name',
required: true,
placeholder: 'my_project_name',
help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
},
{
type: 'text',
name: 'username',
required: true,
help: s_('The username for the Jenkins server.')
},
{
type: 'password',
name: 'password',
help: s_('The password for the Jenkins server.'),
non_empty_password_title: s_('ProjectService|Enter new password.'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
}
]
end
end
end
# frozen_string_literal: true
# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
module Integrations
class MockCi < BaseCi
ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
prop_accessor :mock_service_url
validates :mock_service_url, presence: true, public_url: true, if: :activated?
def title
'MockCI'
end
def description
'Mock an external CI'
end
def self.to_param
'mock_ci'
end
def fields
[
{
type: 'text',
name: 'mock_service_url',
title: s_('ProjectService|Mock service URL'),
placeholder: 'http://localhost:4004',
required: true
}
]
end
# Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
Gitlab::Utils.append_path(
mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}")
end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
# Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
# @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
def commit_status(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
end
def commit_status_path(sha)
Gitlab::Utils.append_path(
mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}.json")
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'pending'
else
response['status']
end
if status.present? && ALLOWED_STATES.include?(status)
status
else
:error
end
end
def can_test?
false
end
end
end
# frozen_string_literal: true
module Integrations
class Teamcity < BaseCi
include ReactiveService
include ServicePushDataValidations
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, public_url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
if: ->(service) { service.activated? && service.username }
attr_accessor :response
after_save :compose_service_hook, if: :activated?
before_update :reset_password
class << self
def to_param
'teamcity'
end
def supported_events
%w(push merge_request)
end
def event_description(event)
case event
when 'push', 'push_events'
'TeamCity CI will be triggered after every push to the repository except branch delete'
when 'merge_request', 'merge_request_events'
'TeamCity CI will be triggered after a merge request has been created or updated'
end
end
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
def reset_password
if teamcity_url_changed? && !password_touched?
self.password = nil
end
end
def title
'JetBrains TeamCity'
end
def description
s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
def help
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
def fields
[
{
type: 'text',
name: 'teamcity_url',
title: s_('ProjectService|TeamCity server URL'),
placeholder: 'https://teamcity.example.com',
required: true
},
{
type: 'text',
name: 'build_type',
help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
required: true
},
{
type: 'text',
name: 'username',
help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
},
{
type: 'password',
name: 'password',
non_empty_password_title: s_('ProjectService|Enter new password'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
}
]
end
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def calculate_reactive_cache(sha, ref)
response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")
if response
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
else
{ build_page: teamcity_url, commit_status: :error }
end
end
def execute(data)
case data[:object_kind]
when 'push'
execute_push(data)
when 'merge_request'
execute_merge_request(data)
end
end
private
def execute_push(data)
branch = Gitlab::Git.ref_name(data[:ref])
post_to_build_queue(data, branch) if push_valid?(data)
end
def execute_merge_request(data)
branch = data[:object_attributes][:source_branch]
post_to_build_queue(data, branch) if merge_request_valid?(data)
end
def read_build_page(response)
if response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = response['build']['id']
build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'Pending'
else
response['build']['status']
end
return :error unless status.present?
if status.include?('SUCCESS')
'success'
elsif status.include?('FAILURE')
'failed'
elsif status.include?('Pending')
'pending'
else
:error
end
end
def build_url(path)
Gitlab::Utils.append_path(teamcity_url, path)
end
def get_path(path)
Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
end
def post_to_build_queue(data, branch)
Gitlab::HTTP.post(
build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=#{branch.encode(xml: :attr)}>"\
"<buildType id=#{build_type.encode(xml: :attr)}/>"\
'</build>',
headers: { 'Content-type' => 'application/xml' },
basic_auth: basic_auth
)
end
def basic_auth
{ username: username, password: password }
end
end
end
...@@ -187,33 +187,33 @@ class Project < ApplicationRecord ...@@ -187,33 +187,33 @@ class Project < ApplicationRecord
has_one :assembla_service, class_name: 'Integrations::Assembla' has_one :assembla_service, class_name: 'Integrations::Assembla'
has_one :bamboo_service, class_name: 'Integrations::Bamboo' has_one :bamboo_service, class_name: 'Integrations::Bamboo'
has_one :bugzilla_service, class_name: 'Integrations::Bugzilla' has_one :bugzilla_service, class_name: 'Integrations::Bugzilla'
has_one :buildkite_service, class_name: 'Integrations::Buildkite'
has_one :campfire_service, class_name: 'Integrations::Campfire' has_one :campfire_service, class_name: 'Integrations::Campfire'
has_one :confluence_service, class_name: 'Integrations::Confluence' has_one :confluence_service, class_name: 'Integrations::Confluence'
has_one :custom_issue_tracker_service, class_name: 'Integrations::CustomIssueTracker' has_one :custom_issue_tracker_service, class_name: 'Integrations::CustomIssueTracker'
has_one :datadog_service, class_name: 'Integrations::Datadog' has_one :datadog_service, class_name: 'Integrations::Datadog'
has_one :drone_ci_service, class_name: 'Integrations::DroneCi'
has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush' has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_service, class_name: 'Integrations::Ewm' has_one :ewm_service, class_name: 'Integrations::Ewm'
has_one :external_wiki_service, class_name: 'Integrations::ExternalWiki' has_one :external_wiki_service, class_name: 'Integrations::ExternalWiki'
has_one :flowdock_service, class_name: 'Integrations::Flowdock' has_one :flowdock_service, class_name: 'Integrations::Flowdock'
has_one :irker_service, class_name: 'Integrations::Irker' has_one :irker_service, class_name: 'Integrations::Irker'
has_one :jenkins_service, class_name: 'Integrations::Jenkins'
has_one :jira_service, class_name: 'Integrations::Jira' has_one :jira_service, class_name: 'Integrations::Jira'
has_one :mock_ci_service, class_name: 'Integrations::MockCi'
has_one :packagist_service, class_name: 'Integrations::Packagist' has_one :packagist_service, class_name: 'Integrations::Packagist'
has_one :pipelines_email_service, class_name: 'Integrations::PipelinesEmail' has_one :pipelines_email_service, class_name: 'Integrations::PipelinesEmail'
has_one :pivotaltracker_service, class_name: 'Integrations::Pivotaltracker' has_one :pivotaltracker_service, class_name: 'Integrations::Pivotaltracker'
has_one :redmine_service, class_name: 'Integrations::Redmine' has_one :redmine_service, class_name: 'Integrations::Redmine'
has_one :teamcity_service, class_name: 'Integrations::Teamcity'
has_one :youtrack_service, class_name: 'Integrations::Youtrack' has_one :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :discord_service has_one :discord_service
has_one :drone_ci_service
has_one :mattermost_slash_commands_service has_one :mattermost_slash_commands_service
has_one :mattermost_service has_one :mattermost_service
has_one :slack_slash_commands_service has_one :slack_slash_commands_service
has_one :slack_service has_one :slack_service
has_one :buildkite_service
has_one :teamcity_service
has_one :pushover_service has_one :pushover_service
has_one :jenkins_service
has_one :prometheus_service, inverse_of: :project has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service
has_one :mock_monitoring_service has_one :mock_monitoring_service
has_one :microsoft_teams_service has_one :microsoft_teams_service
has_one :hangouts_chat_service has_one :hangouts_chat_service
......
# frozen_string_literal: true
require "addressable/uri"
class BuildkiteService < CiService
include ReactiveService
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
validates :project_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def self.supported_events
%w(push merge_request tag_push)
end
# This is a stub method to work with deprecated API response
# TODO: remove enable_ssl_verification after 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/222808
def enable_ssl_verification
true
end
# Since SSL verification will always be enabled for Buildkite,
# we no longer needs to store the boolean.
# This is a stub method to work with deprecated API param.
# TODO: remove enable_ssl_verification after 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/222808
def enable_ssl_verification=(_value)
self.properties.delete('enable_ssl_verification') # Remove unused key
end
def webhook_url
"#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
hook.enable_ssl_verification = true
hook.save
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data)
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def commit_status_path(sha)
"#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
end
def build_page(sha, ref)
"#{project_url}/builds?commit=#{sha}"
end
def title
'Buildkite'
end
def description
'Run CI/CD pipelines with Buildkite.'
end
def self.to_param
'buildkite'
end
def fields
[
{ type: 'text',
name: 'token',
title: 'Integration Token',
help: 'This token will be provided when you create a Buildkite pipeline with a GitLab repository',
required: true },
{ type: 'text',
name: 'project_url',
title: 'Pipeline URL',
placeholder: "#{ENDPOINT}/acme-inc/test-pipeline",
required: true }
]
end
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
status =
if response&.code == 200 && response['status']
response['status']
else
:error
end
{ commit_status: status }
end
private
def webhook_token
token_parts.first
end
def status_token
token_parts.second
end
def token_parts
if token.present?
token.split(':')
else
[]
end
end
def buildkite_endpoint(subdomain = nil)
if subdomain.present?
uri = Addressable::URI.parse(ENDPOINT)
new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
if uri.port.present?
"#{new_endpoint}:#{uri.port}"
else
new_endpoint
end
else
ENDPOINT
end
end
def request_options
{ verify: false, extra_log_info: { project_id: project_id } }
end
end
# frozen_string_literal: true
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab merge requests
class CiService < Integration
default_value_for :category, 'ci'
def valid_token?(token)
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
end
def self.supported_events
%w(push)
end
# Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
# implement inside child
end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
#
# Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
# @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
def commit_status(sha, ref)
# implement inside child
end
end
# frozen_string_literal: true
class DroneCiService < CiService
include ReactiveService
include ServicePushDataValidations
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
# If using a service template, project may not be available
hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
def execute(data)
case data[:object_kind]
when 'push'
service_hook.execute(data) if push_valid?(data)
when 'merge_request'
service_hook.execute(data) if merge_request_valid?(data)
when 'tag_push'
service_hook.execute(data) if tag_push_valid?(data)
end
end
def allow_target_ci?
true
end
def self.supported_events
%w(push merge_request tag_push)
end
def commit_status_path(sha, ref)
Gitlab::Utils.append_path(
drone_url,
"gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) { |cached| cached[:commit_status] }
end
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
verify: enable_ssl_verification,
extra_log_info: { project_id: project_id })
status =
if response && response.code == 200 && response['status']
case response['status']
when 'killed'
:canceled
when 'failure', 'error'
# Because drone return error if some test env failed
:failed
else
response["status"]
end
else
:error
end
{ commit_status: status }
end
def build_page(sha, ref)
Gitlab::Utils.append_path(
drone_url,
"gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
def title
'Drone'
end
def description
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
def self.to_param
'drone_ci'
end
def help
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
def fields
[
{ type: 'text', name: 'token', help: s_('ProjectService|Token for the Drone project.'), required: true },
{ type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
end
# frozen_string_literal: true
class JenkinsService < CiService
include ActionView::Helpers::UrlHelper
prop_accessor :jenkins_url, :project_name, :username, :password
before_update :reset_password
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
default_value_for :push_events, true
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
after_save :compose_service_hook, if: :activated?
def reset_password
# don't reset the password if a new one is provided
if (jenkins_url_changed? || username.blank?) && !password_touched?
self.password = nil
end
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = hook_url
hook.save
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data, "#{data[:object_kind]}_hook")
end
def test(data)
begin
result = execute(data)
return { success: false, result: result[:message] } if result[:http_status] != 200
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result[:message] }
end
def hook_url
url = URI.parse(jenkins_url)
url.path = File.join(url.path || '/', "project/#{project_name}")
url.user = ERB::Util.url_encode(username) unless username.blank?
url.password = ERB::Util.url_encode(password) unless password.blank?
url.to_s
end
def self.supported_events
%w(push merge_request tag_push)
end
def title
'Jenkins'
end
def description
s_('Run CI/CD pipelines with Jenkins.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'jenkins'
end
def fields
[
{
type: 'text',
name: 'jenkins_url',
title: s_('ProjectService|Jenkins server URL'),
required: true,
placeholder: 'http://jenkins.example.com',
help: s_('The URL of the Jenkins server.')
},
{
type: 'text',
name: 'project_name',
required: true,
placeholder: 'my_project_name',
help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
},
{
type: 'text',
name: 'username',
required: true,
help: s_('The username for the Jenkins server.')
},
{
type: 'password',
name: 'password',
help: s_('The password for the Jenkins server.'),
non_empty_password_title: s_('ProjectService|Enter new password.'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
}
]
end
end
# frozen_string_literal: true
# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
class MockCiService < CiService
ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
prop_accessor :mock_service_url
validates :mock_service_url, presence: true, public_url: true, if: :activated?
def title
'MockCI'
end
def description
'Mock an external CI'
end
def self.to_param
'mock_ci'
end
def fields
[
{
type: 'text',
name: 'mock_service_url',
title: s_('ProjectService|Mock service URL'),
placeholder: 'http://localhost:4004',
required: true
}
]
end
# Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
Gitlab::Utils.append_path(
mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}")
end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
#
# Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
# @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
def commit_status(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
end
def commit_status_path(sha)
Gitlab::Utils.append_path(
mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}.json")
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'pending'
else
response['status']
end
if status.present? && ALLOWED_STATES.include?(status)
status
else
:error
end
end
def can_test?
false
end
end
# frozen_string_literal: true
class TeamcityService < CiService
include ReactiveService
include ServicePushDataValidations
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, public_url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
if: ->(service) { service.activated? && service.username }
attr_accessor :response
after_save :compose_service_hook, if: :activated?
before_update :reset_password
class << self
def to_param
'teamcity'
end
def supported_events
%w(push merge_request)
end
def event_description(event)
case event
when 'push', 'push_events'
'TeamCity CI will be triggered after every push to the repository except branch delete'
when 'merge_request', 'merge_request_events'
'TeamCity CI will be triggered after a merge request has been created or updated'
end
end
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
def reset_password
if teamcity_url_changed? && !password_touched?
self.password = nil
end
end
def title
'JetBrains TeamCity'
end
def description
s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
def help
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
def fields
[
{
type: 'text',
name: 'teamcity_url',
title: s_('ProjectService|TeamCity server URL'),
placeholder: 'https://teamcity.example.com',
required: true
},
{
type: 'text',
name: 'build_type',
help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
required: true
},
{
type: 'text',
name: 'username',
help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
},
{
type: 'password',
name: 'password',
non_empty_password_title: s_('ProjectService|Enter new password'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
}
]
end
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def calculate_reactive_cache(sha, ref)
response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")
if response
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
else
{ build_page: teamcity_url, commit_status: :error }
end
end
def execute(data)
case data[:object_kind]
when 'push'
execute_push(data)
when 'merge_request'
execute_merge_request(data)
end
end
private
def execute_push(data)
branch = Gitlab::Git.ref_name(data[:ref])
post_to_build_queue(data, branch) if push_valid?(data)
end
def execute_merge_request(data)
branch = data[:object_attributes][:source_branch]
post_to_build_queue(data, branch) if merge_request_valid?(data)
end
def read_build_page(response)
if response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = response['build']['id']
build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'Pending'
else
response['build']['status']
end
return :error unless status.present?
if status.include?('SUCCESS')
'success'
elsif status.include?('FAILURE')
'failed'
elsif status.include?('Pending')
'pending'
else
:error
end
end
def build_url(path)
Gitlab::Utils.append_path(teamcity_url, path)
end
def get_path(path)
Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
end
def post_to_build_queue(data, branch)
Gitlab::HTTP.post(
build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=#{branch.encode(xml: :attr)}>"\
"<buildType id=#{build_type.encode(xml: :attr)}/>"\
'</build>',
headers: { 'Content-type' => 'application/xml' },
basic_auth: basic_auth
)
end
def basic_auth
{ username: username, password: password }
end
end
...@@ -778,40 +778,40 @@ module API ...@@ -778,40 +778,40 @@ module API
::Integrations::Assembla, ::Integrations::Assembla,
::Integrations::Bamboo, ::Integrations::Bamboo,
::Integrations::Bugzilla, ::Integrations::Bugzilla,
::Integrations::Buildkite,
::Integrations::Campfire, ::Integrations::Campfire,
::Integrations::Confluence, ::Integrations::Confluence,
::Integrations::CustomIssueTracker, ::Integrations::CustomIssueTracker,
::Integrations::Datadog, ::Integrations::Datadog,
::Integrations::DroneCi,
::Integrations::EmailsOnPush, ::Integrations::EmailsOnPush,
::Integrations::Ewm, ::Integrations::Ewm,
::Integrations::ExternalWiki, ::Integrations::ExternalWiki,
::Integrations::Flowdock, ::Integrations::Flowdock,
::Integrations::Irker, ::Integrations::Irker,
::Integrations::Jenkins,
::Integrations::Jira, ::Integrations::Jira,
::Integrations::Packagist, ::Integrations::Packagist,
::Integrations::PipelinesEmail, ::Integrations::PipelinesEmail,
::Integrations::Pivotaltracker, ::Integrations::Pivotaltracker,
::Integrations::Redmine, ::Integrations::Redmine,
::Integrations::Teamcity,
::Integrations::Youtrack, ::Integrations::Youtrack,
::BuildkiteService,
::DiscordService, ::DiscordService,
::DroneCiService,
::HangoutsChatService, ::HangoutsChatService,
::JenkinsService,
::MattermostSlashCommandsService, ::MattermostSlashCommandsService,
::SlackSlashCommandsService, ::SlackSlashCommandsService,
::PrometheusService, ::PrometheusService,
::PushoverService, ::PushoverService,
::SlackService, ::SlackService,
::MattermostService, ::MattermostService,
::MicrosoftTeamsService, ::MicrosoftTeamsService
::TeamcityService
] ]
end end
def self.development_service_classes def self.development_service_classes
[ [
::MockCiService, ::Integrations::MockCi,
::MockMonitoringService ::MockMonitoringService
] ]
end end
......
...@@ -4,9 +4,9 @@ module Gitlab ...@@ -4,9 +4,9 @@ module Gitlab
module Integrations module Integrations
class StiType < ActiveRecord::Type::String class StiType < ActiveRecord::Type::String
NAMESPACED_INTEGRATIONS = Set.new(%w( NAMESPACED_INTEGRATIONS = Set.new(%w(
Asana Assembla Bamboo Bugzilla Campfire Confluence CustomIssueTracker Datadog Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
EmailsOnPush Ewm ExternalWiki Flowdock IssueTracker Irker Jira Packagist PipelinesEmail DroneCi EmailsOnPush Ewm ExternalWiki Flowdock IssueTracker Irker Jenkins Jira MockCi Packagist
Pivotaltracker Redmine Youtrack PipelinesEmail Pivotaltracker Redmine Teamcity Youtrack
)).freeze )).freeze
def cast(value) def cast(value)
......
...@@ -95,7 +95,7 @@ RSpec.describe Projects::ServicesController do ...@@ -95,7 +95,7 @@ RSpec.describe Projects::ServicesController do
expect(response).to be_successful expect(response).to be_successful
expect(json_response).to be_empty expect(json_response).to be_empty
expect(BuildkiteService.first).to be_present expect(Integrations::Buildkite.first).to be_present
end end
it 'creates the ServiceHook object' do it 'creates the ServiceHook object' do
...@@ -103,7 +103,7 @@ RSpec.describe Projects::ServicesController do ...@@ -103,7 +103,7 @@ RSpec.describe Projects::ServicesController do
expect(response).to be_successful expect(response).to be_successful
expect(json_response).to be_empty expect(json_response).to be_empty
expect(BuildkiteService.first.service_hook).to be_present expect(Integrations::Buildkite.first.service_hook).to be_present
end end
def do_put def do_put
......
...@@ -38,7 +38,7 @@ FactoryBot.define do ...@@ -38,7 +38,7 @@ FactoryBot.define do
end end
end end
factory :drone_ci_service do factory :drone_ci_service, class: 'Integrations::DroneCi' do
project project
active { true } active { true }
drone_url { 'https://bamboo.example.com' } drone_url { 'https://bamboo.example.com' }
......
...@@ -104,7 +104,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do ...@@ -104,7 +104,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
before do before do
create(:service, project: project, create(:service, project: project,
active: true, active: true,
type: 'CiService', type: 'DroneCiService',
category: 'ci') category: 'ci')
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
......
...@@ -15,7 +15,7 @@ RSpec.describe 'Disable individual triggers', :js do ...@@ -15,7 +15,7 @@ RSpec.describe 'Disable individual triggers', :js do
let(:service_name) { 'Jenkins' } let(:service_name) { 'Jenkins' }
it 'shows trigger checkboxes' do it 'shows trigger checkboxes' do
event_count = JenkinsService.supported_events.count event_count = Integrations::Jenkins.supported_events.count
expect(page).to have_content "Trigger" expect(page).to have_content "Trigger"
expect(page).to have_css(checkbox_selector, visible: :all, count: event_count) expect(page).to have_css(checkbox_selector, visible: :all, count: event_count)
......
...@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do ...@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do
end end
context 'when models using single-type inheritance are used' do context 'when models using single-type inheritance are used' do
let(:models) { [Group, CiService, Namespace] } let(:models) { [Group, Integrations::BaseCi, Namespace] }
before do before do
models.each do |model| models.each do |model|
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers include ReactiveCachingHelpers
include StubRequests include StubRequests
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers include ReactiveCachingHelpers
describe 'associations' do describe 'associations' do
...@@ -32,7 +32,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do ...@@ -32,7 +32,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
end end
shared_context :drone_ci_service do shared_context :drone_ci_service do
let(:drone) { DroneCiService.new } let(:drone) { described_class.new }
let(:project) { create(:project, :repository, name: 'project') } let(:project) { create(:project, :repository, name: 'project') }
let(:path) { project.full_path } let(:path) { project.full_path }
let(:drone_url) { 'http://drone.example.com' } let(:drone_url) { 'http://drone.example.com' }
...@@ -41,7 +41,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do ...@@ -41,7 +41,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
let(:token) { 'secret' } let(:token) { 'secret' }
let(:iid) { rand(1..9999) } let(:iid) { rand(1..9999) }
# URL's # URLs
let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe JenkinsService do RSpec.describe Integrations::Jenkins do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:jenkins_url) { 'http://jenkins.example.com/' } let(:jenkins_url) { 'http://jenkins.example.com/' }
let(:jenkins_hook_url) { jenkins_url + 'project/my_project' } let(:jenkins_hook_url) { jenkins_url + 'project/my_project' }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers include ReactiveCachingHelpers
include StubRequests include StubRequests
......
...@@ -12,7 +12,7 @@ RSpec.describe MergeRequestPresenter do ...@@ -12,7 +12,7 @@ RSpec.describe MergeRequestPresenter do
context 'when no head pipeline' do context 'when no head pipeline' do
it 'return status using CiService' do it 'return status using CiService' do
ci_service = double(MockCiService) ci_service = double(Integrations::MockCi)
ci_status = double ci_status = double
allow(resource.source_project) allow(resource.source_project)
......
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