diff options
Diffstat (limited to 'app/models/project_services')
39 files changed, 2 insertions, 3076 deletions
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb deleted file mode 100644 index d1c56d2a4d5..00000000000 --- a/app/models/project_services/bugzilla_service.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -class BugzillaService < IssueTrackerService - include ActionView::Helpers::UrlHelper - - validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - - def title - 'Bugzilla' - end - - def description - s_("IssueTracker|Use Bugzilla as this project's issue tracker.") - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer' - s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'bugzilla' - end -end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb deleted file mode 100644 index f2ea5066e37..00000000000 --- a/app/models/project_services/buildkite_service.rb +++ /dev/null @@ -1,143 +0,0 @@ -# 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 diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb deleted file mode 100644 index 2f841bf903e..00000000000 --- a/app/models/project_services/chat_notification_service.rb +++ /dev/null @@ -1,252 +0,0 @@ -# frozen_string_literal: true - -# Base class for Chat notifications services -# This class is not meant to be used directly, but only to inherit from. -class ChatNotificationService < Integration - include ChatMessage - include NotificationBranchSelection - - SUPPORTED_EVENTS = %w[ - push issue confidential_issue merge_request note confidential_note - tag_push pipeline wiki_page deployment - ].freeze - - SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze - - EVENT_CHANNEL = proc { |event| "#{event}_channel" } - - LABEL_NOTIFICATION_BEHAVIOURS = [ - MATCH_ANY_LABEL = 'match_any', - MATCH_ALL_LABELS = 'match_all' - ].freeze - - default_value_for :category, 'chat' - - prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior - - # Custom serialized properties initialization - prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) - - boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch - - validates :webhook, presence: true, public_url: true, if: :activated? - validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true - - def initialize_properties - if properties.nil? - self.properties = {} - self.notify_only_broken_pipelines = true - self.branches_to_be_notified = "default" - self.labels_to_be_notified_behavior = MATCH_ANY_LABEL - elsif !self.notify_only_default_branch.nil? - # In older versions, there was only a boolean property named - # `notify_only_default_branch`. Now we have a string property named - # `branches_to_be_notified`. Instead of doing a background migration, we - # opted to set a value for the new property based on the old one, if - # users hasn't specified one already. When users edit the service and - # selects a value for this new property, it will override everything. - - self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all" - end - end - - def confidential_issue_channel - properties['confidential_issue_channel'].presence || properties['issue_channel'] - end - - def confidential_note_channel - properties['confidential_note_channel'].presence || properties['note_channel'] - end - - def self.supported_events - SUPPORTED_EVENTS - end - - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze, - { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze, - { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze, - { - type: 'text', - name: 'labels_to_be_notified', - placeholder: '~backend,~frontend', - help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' - }.freeze, - { - type: 'select', - name: 'labels_to_be_notified_behavior', - choices: [ - ['Match any of the labels', MATCH_ANY_LABEL], - ['Match all of the labels', MATCH_ALL_LABELS] - ] - }.freeze - ].freeze - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - return unless notify_label?(data) - - return unless webhook.present? - - object_kind = data[:object_kind] - - data = custom_data(data) - - # WebHook events often have an 'update' event that follows a 'open' or - # 'close' action. Ignore update events for now to prevent duplicate - # messages from arriving. - - message = get_message(object_kind, data) - - return false unless message - - event_type = data[:event_type] || object_kind - - channel_names = get_channel_field(event_type).presence || channel.presence - channels = channel_names&.split(',')&.map(&:strip) - - opts = {} - opts[:channel] = channels if channels.present? - opts[:username] = username if username - - if notify(message, opts) - log_usage(event_type, user_id_from_hook_data(data)) - return true - end - - false - end - - def event_channel_names - supported_events.map { |event| event_channel_name(event) } - end - - def event_field(event) - fields.find { |field| field[:name] == event_channel_name(event) } - end - - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - def default_channel_placeholder - raise NotImplementedError - end - - private - - def log_usage(_, _) - # Implement in child class - end - - def labels_to_be_notified_list - return [] if labels_to_be_notified.nil? - - labels_to_be_notified.delete('~').split(',').map(&:strip) - end - - def notify_label?(data) - return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present? - - labels = data.dig(:issue, :labels) || data.dig(:merge_request, :labels) - - return false if labels.nil? - - matching_labels = labels_to_be_notified_list & labels.pluck(:title) - - if labels_to_be_notified_behavior == MATCH_ALL_LABELS - labels_to_be_notified_list.difference(matching_labels).empty? - else - matching_labels.any? - end - end - - def user_id_from_hook_data(data) - data.dig(:user, :id) || data[:user_id] - end - - # every notifier must implement this independently - def notify(message, opts) - raise NotImplementedError - end - - def custom_data(data) - data.merge(project_url: project_url, project_name: project_name) - end - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data) - when "issue" - Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) - when "merge_request" - Integrations::ChatMessage::MergeMessage.new(data) unless update?(data) - when "note" - Integrations::ChatMessage::NoteMessage.new(data) - when "pipeline" - Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - Integrations::ChatMessage::WikiPageMessage.new(data) - when "deployment" - Integrations::ChatMessage::DeploymentMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder } - end - end - - def event_channel_name(event) - EVENT_CHANNEL[event] - end - - def project_name - project.full_name - end - - def project_url - project.web_url - end - - def update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_pipeline_be_notified?(data) - notify_for_ref?(data) && notify_for_pipeline?(data) - end - - def notify_for_ref?(data) - return true if data[:object_kind] == 'tag_push' - return true if data.dig(:object_attributes, :tag) - - notify_for_branch?(data) - end - - def notify_for_pipeline?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end - end -end diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb deleted file mode 100644 index 0733da761d5..00000000000 --- a/app/models/project_services/ci_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -# 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 diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb deleted file mode 100644 index 6f99d104904..00000000000 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class CustomIssueTrackerService < IssueTrackerService - include ActionView::Helpers::UrlHelper - validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - - def title - s_('IssueTracker|Custom issue tracker') - end - - def description - s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.") - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer' - s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'custom_issue_tracker' - end -end diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb deleted file mode 100644 index ca4dc0375fb..00000000000 --- a/app/models/project_services/data_fields.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module DataFields - extend ActiveSupport::Concern - - class_methods do - # Provide convenient accessor methods for data fields. - # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 - def data_field(*args) - args.each do |arg| - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - unless method_defined?(arg) - def #{arg} - data_fields.send('#{arg}') || (properties && properties['#{arg}']) - end - end - - def #{arg}=(value) - @old_data_fields ||= {} - @old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only - data_fields.send('#{arg}=', value) - end - - def #{arg}_touched? - @old_data_fields ||= {} - @old_data_fields.has_key?('#{arg}') - end - - def #{arg}_changed? - #{arg}_touched? && @old_data_fields['#{arg}'] != #{arg} - end - - def #{arg}_was - return unless #{arg}_touched? - return if data_fields.persisted? # arg_was does not work for attr_encrypted - - legacy_properties_data['#{arg}'] - end - RUBY - end - end - end - - included do - has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id - has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id - has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id - - def data_fields - raise NotImplementedError - end - - def data_fields_present? - data_fields.present? - rescue NotImplementedError - false - end - end -end diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb deleted file mode 100644 index d7adf63fde4..00000000000 --- a/app/models/project_services/discord_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require "discordrb/webhooks" - -class DiscordService < ChatNotificationService - include ActionView::Helpers::UrlHelper - - ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze - - def title - s_("DiscordService|Discord Notifications") - end - - def description - s_("DiscordService|Send notifications about project events to a Discord channel.") - end - - def self.to_param - "discord" - end - - def help - docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' - s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def event_field(event) - # No-op. - end - - def default_channel_placeholder - # No-op. - end - - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] - end - - def default_fields - [ - { type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." }, - { type: "checkbox", name: "notify_only_broken_pipelines" }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } - ] - end - - private - - def notify(message, opts) - client = Discordrb::Webhooks::Client.new(url: webhook) - - client.execute do |builder| - builder.add_embed do |embed| - embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar) - embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n") - end - end - rescue RestClient::Exception => error - log_error(error.message) - false - end - - def custom_data(data) - super(data).merge(markdown: true) - end -end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb deleted file mode 100644 index ab1ba768a8f..00000000000 --- a/app/models/project_services/drone_ci_service.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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 diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb deleted file mode 100644 index 90fcbb10d2b..00000000000 --- a/app/models/project_services/ewm_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class EwmService < IssueTrackerService - include ActionView::Helpers::UrlHelper - - validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - - def self.reference_pattern(only_long: true) - @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i - end - - def title - 'EWM' - end - - def description - s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.") - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer' - s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'ewm' - end - - def can_test? - false - end - - def issue_url(iid) - issues_url.gsub(':id', iid.to_s.split(' ')[-1]) - end -end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb deleted file mode 100644 index f49b008533d..00000000000 --- a/app/models/project_services/external_wiki_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class ExternalWikiService < Integration - include ActionView::Helpers::UrlHelper - - prop_accessor :external_wiki_url - validates :external_wiki_url, presence: true, public_url: true, if: :activated? - - def title - s_('ExternalWikiService|External wiki') - end - - def description - s_('ExternalWikiService|Link to an external wiki from the sidebar.') - end - - def self.to_param - 'external_wiki' - end - - def fields - [ - { - type: 'text', - name: 'external_wiki_url', - title: s_('ExternalWikiService|External wiki URL'), - placeholder: s_('ExternalWikiService|https://example.com/xxx/wiki/...'), - help: 'Enter the URL to the external wiki.', - required: true - } - ] - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer' - - s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def execute(_data) - response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) - response.body if response.code == 200 - rescue StandardError - nil - end - - def self.supported_events - %w() - end -end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb deleted file mode 100644 index 7aae5af7454..00000000000 --- a/app/models/project_services/flowdock_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class FlowdockService < Integration - include ActionView::Helpers::UrlHelper - - prop_accessor :token - validates :token, presence: true, if: :activated? - - def title - 'Flowdock' - end - - def description - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.') - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer' - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'flowdock' - end - - def fields - [ - { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' } - ] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - Flowdock::Git.post( - data[:ref], - data[:before], - data[:after], - token: token, - repo: project.repository, - repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", - commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s", - diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" - ) - end -end diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb deleted file mode 100644 index 6e7708a169f..00000000000 --- a/app/models/project_services/hangouts_chat_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'hangouts_chat' - -class HangoutsChatService < ChatNotificationService - include ActionView::Helpers::UrlHelper - - def title - 'Google Chat' - end - - def description - 'Send notifications from GitLab to a room in Google Chat.' - end - - def self.to_param - 'hangouts_chat' - end - - def help - docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer' - s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def event_field(event) - end - - def default_channel_placeholder - end - - def webhook_placeholder - 'https://chat.googleapis.com/v1/spaces…' - end - - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } - ] - end - - private - - def notify(message, opts) - simple_text = parse_simple_text_message(message) - HangoutsChat::Sender.new(webhook).simple(simple_text) - end - - def parse_simple_text_message(message) - header = message.pretext - return header if message.attachments.empty? - - attachment = message.attachments.first - title = format_attachment_title(attachment) - body = attachment[:text] - - [header, title, body].compact.join("\n") - end - - def format_attachment_title(attachment) - return attachment[:title] unless attachment[:title_link] - - "<#{attachment[:title_link]}|#{attachment[:title]}>" - end -end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb deleted file mode 100644 index 71d8e7bfac4..00000000000 --- a/app/models/project_services/hipchat_service.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# This service is scheduled for removal. All records must -# be deleted before the class can be removed. -# https://gitlab.com/gitlab-org/gitlab/-/issues/27954 -class HipchatService < Integration - before_save :prevent_save - - def self.to_param - 'hipchat' - end - - def self.supported_events - [] - end - - def execute(data) - # We removed the hipchat gem due to https://gitlab.com/gitlab-org/gitlab/-/issues/325851#note_537143149 - # HipChat is unusable anyway, so do nothing in this method - end - - private - - def prevent_save - errors.add(:base, _('HipChat endpoint is deprecated and should not be created or modified.')) - - # Stops execution of callbacks and database operation while - # preserving expectations of #save (will not raise) & #save! (raises) - # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution - throw :abort # rubocop:disable Cop/BanCatchThrow - end -end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb deleted file mode 100644 index 5cca620c659..00000000000 --- a/app/models/project_services/irker_service.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -require 'uri' - -class IrkerService < Integration - prop_accessor :server_host, :server_port, :default_irc_uri - prop_accessor :recipients, :channels - boolean_accessor :colorize_messages - validates :recipients, presence: true, if: :validate_recipients? - - before_validation :get_channels - - def title - 'Irker (IRC gateway)' - end - - def description - 'Send IRC messages.' - end - - def self.to_param - 'irker' - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - IrkerWorker.perform_async(project_id, channels, - colorize_messages, data, settings) - end - - def settings - { - server_host: server_host.presence || 'localhost', - server_port: server_port.presence || 6659 - } - end - - def fields - [ - { type: 'text', name: 'server_host', placeholder: 'localhost', - help: 'Irker daemon hostname (defaults to localhost)' }, - { type: 'text', name: 'server_port', placeholder: 6659, - help: 'Irker daemon port (defaults to 6659)' }, - { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI', - help: 'A default IRC URI to prepend before each recipient (optional)', - placeholder: 'irc://irc.network.net:6697/' }, - { type: 'textarea', name: 'recipients', - placeholder: 'Recipients/channels separated by whitespaces', required: true, - help: 'Recipients have to be specified with a full URI: '\ - 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\ - 'you want the channel to be a nickname instead, append ",isnick" to ' \ - 'the channel name; if the channel is protected by a secret password, ' \ - ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \ - ' want to use a password, you have to omit the "#" on the channel). If you ' \ - ' specify a default IRC URI to prepend before each recipient, you can just ' \ - ' give a channel name.' }, - { type: 'checkbox', name: 'colorize_messages' } - ] - end - - def help - ' NOTE: Irker does NOT have built-in authentication, which makes it' \ - ' vulnerable to spamming IRC channels if it is hosted outside of a ' \ - ' firewall. Please make sure you run the daemon within a secured network ' \ - ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.' - end - - private - - def get_channels - return true unless activated? - return true if recipients.nil? || recipients.empty? - - map_recipients - - errors.add(:recipients, 'are all invalid') if channels.empty? - true - end - - def map_recipients - self.channels = recipients.split(/\s+/).map do |recipient| - format_channel(recipient) - end - channels.reject!(&:nil?) - end - - def format_channel(recipient) - uri = nil - - # Try to parse the chan as a full URI - begin - uri = consider_uri(URI.parse(recipient)) - rescue URI::InvalidURIError - end - - unless uri.present? && default_irc_uri.nil? - begin - new_recipient = URI.join(default_irc_uri, '/', recipient).to_s - uri = consider_uri(URI.parse(new_recipient)) - rescue StandardError - log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient) - end - end - - uri - end - - def consider_uri(uri) - return if uri.scheme.nil? - - # Authorize both irc://domain.com/#chan and irc://domain.com/chan - if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? - uri.to_s - end - end -end diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb deleted file mode 100644 index 414f2c1da4d..00000000000 --- a/app/models/project_services/issue_tracker_data.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class IssueTrackerData < ApplicationRecord - include Services::DataFields - - attr_encrypted :project_url, encryption_options - attr_encrypted :issues_url, encryption_options - attr_encrypted :new_issue_url, encryption_options -end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb deleted file mode 100644 index 099e3c336dd..00000000000 --- a/app/models/project_services/issue_tracker_service.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -class IssueTrackerService < Integration - validate :one_issue_tracker, if: :activated?, on: :manual_change - - # TODO: we can probably just delegate as part of - # https://gitlab.com/gitlab-org/gitlab/issues/29404 - data_field :project_url, :issues_url, :new_issue_url - - default_value_for :category, 'issue_tracker' - - before_validation :handle_properties - before_validation :set_default_data, on: :create - - # Pattern used to extract links from comments - # Override this method on services that uses different patterns - # This pattern does not support cross-project references - # The other code assumes that this pattern is a superset of all - # overridden patterns. See ReferenceRegexes.external_pattern - def self.reference_pattern(only_long: false) - if only_long - /(\b[A-Z][A-Z0-9_]*-)#{Gitlab::Regex.issue}/ - else - /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})#{Gitlab::Regex.issue}/ - end - end - - def handle_properties - # this has been moved from initialize_properties and should be improved - # as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 - return unless properties - - @legacy_properties_data = properties.dup - data_values = properties.slice!('title', 'description') - data_values.reject! { |key| data_fields.changed.include?(key) } - data_values.slice!(*data_fields.attributes.keys) - data_fields.assign_attributes(data_values) if data_values.present? - - self.properties = {} - end - - def legacy_properties_data - @legacy_properties_data ||= {} - end - - def supports_data_fields? - true - end - - def data_fields - issue_tracker_data || self.build_issue_tracker_data - end - - def default? - default - end - - def issue_url(iid) - issues_url.gsub(':id', iid.to_s) - end - - def issue_tracker_path - project_url - end - - def new_issue_path - new_issue_url - end - - def issue_path(iid) - issue_url(iid) - end - - def fields - [ - { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in the external issue tracker.'), required: true }, - { type: 'text', name: 'issues_url', title: s_('IssueTracker|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }, - { type: 'text', name: 'new_issue_url', title: s_('IssueTracker|New issue URL'), help: s_('IssueTracker|The URL to create an issue in the external issue tracker.'), required: true } - ] - end - - def initialize_properties - {} - end - - # Initialize with default properties values - def set_default_data - return unless issues_tracker.present? - - # we don't want to override if we have set something - return if project_url || issues_url || new_issue_url - - data_fields.project_url = issues_tracker['project_url'] - data_fields.issues_url = issues_tracker['issues_url'] - data_fields.new_issue_url = issues_tracker['new_issue_url'] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." - result = false - - begin - response = Gitlab::HTTP.head(self.project_url, verify: true) - - if response - message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" - result = true - end - rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error - message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" - end - log_info(message) - result - end - - def support_close_issue? - false - end - - def support_cross_reference? - false - end - - private - - def enabled_in_gitlab_config - Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker - end - - def issues_tracker - Gitlab.config.issues_tracker[to_param] - end - - def one_issue_tracker - return if template? || instance? - return if project.blank? - - if project.integrations.external_issue_trackers.where.not(id: id).any? - errors.add(:base, _('Another issue tracker is already in use. Only one issue tracker service can be active at a time')) - end - end -end - -IssueTrackerService.prepend_mod_with('IssueTrackerService') diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb deleted file mode 100644 index 990a35cd617..00000000000 --- a/app/models/project_services/jenkins_service.rb +++ /dev/null @@ -1,111 +0,0 @@ -# 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 diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb deleted file mode 100644 index 5cd6e79eb1d..00000000000 --- a/app/models/project_services/jira_service.rb +++ /dev/null @@ -1,541 +0,0 @@ -# frozen_string_literal: true - -# Accessible as Project#external_issue_tracker -class JiraService < IssueTrackerService - extend ::Gitlab::Utils::Override - include Gitlab::Routing - include ApplicationHelper - include ActionView::Helpers::AssetUrlHelper - include Gitlab::Utils::StrongMemoize - - PROJECTS_PER_PAGE = 50 - - # TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged - DEPLOYMENT_TYPES = { - server: 'SERVER', - cloud: 'CLOUD' - }.freeze - - validates :url, public_url: true, presence: true, if: :activated? - validates :api_url, public_url: true, allow_blank: true - validates :username, presence: true, if: :activated? - validates :password, presence: true, if: :activated? - - validates :jira_issue_transition_id, - format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") }, - allow_blank: true - - # Jira Cloud version is deprecating authentication via username and password. - # We should use username/password for Jira Server and email/api_token for Jira Cloud, - # for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936. - - # TODO: we can probably just delegate as part of - # https://gitlab.com/gitlab-org/gitlab/issues/29404 - data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled, - :vulnerabilities_enabled, :vulnerabilities_issuetype - - before_update :reset_password - after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type? - - enum comment_detail: { - standard: 1, - all_details: 2 - } - - alias_method :project_url, :url - - # When these are false GitLab does not create cross reference - # comments on Jira except when an issue gets transitioned. - def self.supported_events - %w(commit merge_request) - end - - def self.supported_event_actions - %w(comment) - end - - # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def self.reference_pattern(only_long: true) - @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/ - end - - def initialize_properties - {} - end - - def data_fields - jira_tracker_data || self.build_jira_tracker_data - end - - def reset_password - data_fields.password = nil if reset_password? - end - - def set_default_data - return unless issues_tracker.present? - - return if url - - data_fields.url ||= issues_tracker['url'] - data_fields.api_url ||= issues_tracker['api_url'] - end - - def options - url = URI.parse(client_url) - - { - username: username&.strip, - password: password, - site: URI.join(url, '/').to_s, # Intended to find the root - context_path: url.path, - auth_type: :basic, - read_timeout: 120, - use_cookies: true, - additional_cookies: ['OBBasicAuth=fromDialog'], - use_ssl: url.scheme == 'https' - } - end - - def client - @client ||= begin - JIRA::Client.new(options).tap do |client| - # Replaces JIRA default http client with our implementation - client.request_client = Gitlab::Jira::HttpClient.new(client.options) - end - end - end - - def help - jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') } - s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe } - end - - def title - 'Jira' - end - - def description - s_("JiraService|Use Jira as this project's issue tracker.") - end - - def self.to_param - 'jira' - end - - def fields - [ - { - type: 'text', - name: 'url', - title: s_('JiraService|Web URL'), - placeholder: 'https://jira.example.com', - help: s_('JiraService|Base URL of the Jira instance.'), - required: true - }, - { - type: 'text', - name: 'api_url', - title: s_('JiraService|Jira API URL'), - help: s_('JiraService|If different from Web URL.') - }, - { - type: 'text', - name: 'username', - title: s_('JiraService|Username or Email'), - help: s_('JiraService|Use a username for server version and an email for cloud version.'), - required: true - }, - { - type: 'password', - name: 'password', - title: s_('JiraService|Password or API token'), - non_empty_password_title: s_('JiraService|Enter new password or API token'), - non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token.'), - help: s_('JiraService|Use a password for server version and an API token for cloud version.'), - required: true - } - ] - end - - def issues_url - "#{url}/browse/:id" - end - - def new_issue_url - "#{url}/secure/CreateIssue!default.jspa" - end - - alias_method :original_url, :url - def url - original_url&.delete_suffix('/') - end - - alias_method :original_api_url, :api_url - def api_url - original_api_url&.delete_suffix('/') - end - - def execute(push) - # This method is a no-op, because currently JiraService does not - # support any events. - end - - def find_issue(issue_key, rendered_fields: false, transitions: false) - expands = [] - expands << 'renderedFields' if rendered_fields - expands << 'transitions' if transitions - options = { expand: expands.join(',') } if expands.any? - - jira_request { client.Issue.find(issue_key, options || {}) } - end - - def close_issue(entity, external_issue, current_user) - issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic) - - return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled? - - commit_id = case entity - when Commit then entity.id - when MergeRequest then entity.diff_head_sha - end - - commit_url = build_entity_url(:commit, commit_id) - - # Depending on the Jira project's workflow, a comment during transition - # may or may not be allowed. Refresh the issue after transition and check - # if it is closed, so we don't have one comment for every commit. - issue = find_issue(issue.key) if transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) - log_usage(:close_issue, current_user) - end - - def create_cross_reference_note(mentioned, noteable, author) - unless can_cross_reference?(noteable) - return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) } - end - - jira_issue = find_issue(mentioned.id) - - return unless jira_issue.present? - - noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id - noteable_type = noteable_name(noteable) - entity_url = build_entity_url(noteable_type, noteable_id) - entity_meta = build_entity_meta(noteable) - - data = { - user: { - name: author.name, - url: resource_url(user_path(author)) - }, - project: { - name: project.full_path, - url: resource_url(project_path(project)) - }, - entity: { - id: entity_meta[:id], - name: noteable_type.humanize.downcase, - url: entity_url, - title: noteable.title, - description: entity_meta[:description], - branch: entity_meta[:branch] - } - } - - add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) } - end - - def valid_connection? - test(nil)[:success] - end - - def test(_) - result = server_info - success = result.present? - result = @error&.message unless success - - { success: success, result: result } - end - - override :support_close_issue? - def support_close_issue? - true - end - - override :support_cross_reference? - def support_cross_reference? - true - end - - def issue_transition_enabled? - jira_issue_transition_automatic || jira_issue_transition_id.present? - end - - private - - def server_info - strong_memoize(:server_info) do - client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil - end - end - - def can_cross_reference?(noteable) - case noteable - when Commit then commit_events - when MergeRequest then merge_requests_events - else true - end - end - - # jira_issue_transition_id can have multiple values split by , or ; - # the issue is transitioned at the order given by the user - # if any transition fails it will log the error message and stop the transition sequence - def transition_issue(issue) - return transition_issue_to_done(issue) if jira_issue_transition_automatic - - jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id| - transition_issue_to_id(issue, transition_id) - end - end - - def transition_issue_to_id(issue, transition_id) - issue.transitions.build.save!( - transition: { id: transition_id } - ) - - true - rescue StandardError => error - log_error( - "Issue transition failed", - error: { - exception_class: error.class.name, - exception_message: error.message, - exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) - }, - client_url: client_url - ) - - false - end - - def transition_issue_to_done(issue) - transitions = issue.transitions rescue [] - - transition = transitions.find do |transition| - status = transition&.to&.statusCategory - status && status['key'] == 'done' - end - - return false unless transition - - transition_issue_to_id(issue, transition.id) - end - - def log_usage(action, user) - key = "i_ecosystem_jira_service_#{action}" - - Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id) - end - - def add_issue_solved_comment(issue, commit_id, commit_url) - link_title = "Solved by commit #{commit_id}." - comment = "Issue solved with [#{commit_id}|#{commit_url}]." - link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true) - send_message(issue, comment, link_props) - end - - def add_comment(data, issue) - entity_name = data[:entity][:name] - entity_url = data[:entity][:url] - entity_title = data[:entity][:title] - - message = comment_message(data) - link_title = "#{entity_name.capitalize} - #{entity_title}" - link_props = build_remote_link_props(url: entity_url, title: link_title) - - unless comment_exists?(issue, message) - send_message(issue, message, link_props) - end - end - - def comment_message(data) - user_link = build_jira_link(data[:user][:name], data[:user][:url]) - - entity = data[:entity] - entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}" - entity_link = build_jira_link(entity_ref, entity[:url]) - - project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project)) - branch = - if entity[:branch].present? - s_('JiraService| on branch %{branch_link}') % { - branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch])) - } - end - - entity_message = entity[:description].presence if all_details? - entity_message ||= entity[:title].chomp - - s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % { - user_link: user_link, - entity_link: entity_link, - project_link: project_link, - branch: branch, - entity_message: entity_message - } - end - - def build_jira_link(title, url) - "[#{title}|#{url}]" - end - - def has_resolution?(issue) - issue.respond_to?(:resolution) && issue.resolution.present? - end - - def comment_exists?(issue, message) - comments = jira_request { issue.comments } - - comments.present? && comments.any? { |comment| comment.body.include?(message) } - end - - def send_message(issue, message, remote_link_props) - return unless client_url.present? - - jira_request do - remote_link = find_remote_link(issue, remote_link_props[:object][:url]) - - create_issue_comment(issue, message) unless remote_link - remote_link ||= issue.remotelink.build - remote_link.save!(remote_link_props) - - log_info("Successfully posted", client_url: client_url) - "SUCCESS: Successfully posted to #{client_url}." - end - end - - def create_issue_comment(issue, message) - return unless comment_on_event_enabled - - issue.comments.build.save!(body: message) - end - - def find_remote_link(issue, url) - links = jira_request { issue.remotelink.all } - return unless links - - links.find { |link| link.object["url"] == url } - end - - def build_remote_link_props(url:, title:, resolved: false) - status = { - resolved: resolved - } - - { - GlobalID: 'GitLab', - relationship: 'mentioned on', - object: { - url: url, - title: title, - status: status, - icon: { - title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.base_url) - } - } - } - end - - def resource_url(resource) - "#{Settings.gitlab.base_url.chomp("/")}#{resource}" - end - - def build_entity_url(noteable_type, entity_id) - polymorphic_url( - [ - self.project, - noteable_type.to_sym - ], - id: entity_id, - host: Settings.gitlab.base_url - ) - end - - def build_entity_meta(noteable) - if noteable.is_a?(Commit) - { - id: noteable.short_id, - description: noteable.safe_message, - branch: noteable.ref_names(project.repository).first - } - elsif noteable.is_a?(MergeRequest) - { - id: noteable.to_reference, - branch: noteable.source_branch - } - else - {} - end - end - - def noteable_name(noteable) - name = noteable.model_name.singular - - # ProjectSnippet inherits from Snippet class so it causes - # routing error building the URL. - name == "project_snippet" ? "snippet" : name - end - - # Handle errors when doing Jira API calls - def jira_request - yield - rescue StandardError => error - @error = error - log_error("Error sending message", client_url: client_url, error: @error.message) - nil - end - - def client_url - api_url.presence || url - end - - def reset_password? - # don't reset the password if a new one is provided - return false if password_touched? - return true if api_url_changed? - return false if api_url.present? - - url_changed? - end - - def update_deployment_type? - (api_url_changed? || url_changed? || username_changed? || password_changed?) && - can_test? - end - - def update_deployment_type - clear_memoization(:server_info) # ensure we run the request when we try to update deployment type - results = server_info - return data_fields.deployment_unknown! unless results.present? - - case results['deploymentType'] - when 'Server' - data_fields.deployment_server! - when 'Cloud' - data_fields.deployment_cloud! - else - data_fields.deployment_unknown! - end - end - - def self.event_description(event) - case event - when "merge_request", "merge_request_events" - s_("JiraService|Jira comments will be created when an issue gets referenced in a merge request.") - when "commit", "commit_events" - s_("JiraService|Jira comments will be created when an issue gets referenced in a commit.") - end - end -end - -JiraService.prepend_mod_with('JiraService') diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb deleted file mode 100644 index 2c145abf5c9..00000000000 --- a/app/models/project_services/jira_tracker_data.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -class JiraTrackerData < ApplicationRecord - include Services::DataFields - include IgnorableColumns - - ignore_columns %i[ - encrypted_proxy_address - encrypted_proxy_address_iv - encrypted_proxy_port - encrypted_proxy_port_iv - encrypted_proxy_username - encrypted_proxy_username_iv - encrypted_proxy_password - encrypted_proxy_password_iv - ], remove_with: '14.0', remove_after: '2021-05-22' - - attr_encrypted :url, encryption_options - attr_encrypted :api_url, encryption_options - attr_encrypted :username, encryption_options - attr_encrypted :password, encryption_options - - enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment -end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb deleted file mode 100644 index 732a7c32a03..00000000000 --- a/app/models/project_services/mattermost_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class MattermostService < ChatNotificationService - include SlackMattermost::Notifier - include ActionView::Helpers::UrlHelper - - def title - s_('Mattermost notifications') - end - - def description - s_('Send notifications about project events to Mattermost channels.') - end - - def self.to_param - 'mattermost' - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer' - s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def default_channel_placeholder - 'my-channel' - end - - def webhook_placeholder - 'http://mattermost.example.com/hooks/' - end -end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb deleted file mode 100644 index 60235a09dcd..00000000000 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -class MattermostSlashCommandsService < SlashCommandsService - include Ci::TriggersHelper - - prop_accessor :token - - def can_test? - false - end - - def title - 'Mattermost slash commands' - end - - def description - "Perform common tasks with slash commands." - end - - def self.to_param - 'mattermost_slash_commands' - end - - def configure(user, params) - token = Mattermost::Command.new(user) - .create(command(params)) - - update(active: true, token: token) if token - rescue Mattermost::Error => e - [false, e.message] - end - - def list_teams(current_user) - [Mattermost::Team.new(current_user).all, nil] - rescue Mattermost::Error => e - [[], e.message] - end - - def chat_responder - ::Gitlab::Chat::Responder::Mattermost - end - - private - - def command(params) - pretty_project_name = project.full_name - - params.merge( - auto_complete: true, - auto_complete_desc: "Perform common operations on: #{pretty_project_name}", - auto_complete_hint: '[help]', - description: "Perform common operations on: #{pretty_project_name}", - display_name: "GitLab / #{pretty_project_name}", - method: 'P', - username: 'GitLab') - end -end diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb deleted file mode 100644 index 1d2067067da..00000000000 --- a/app/models/project_services/microsoft_teams_service.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -class MicrosoftTeamsService < ChatNotificationService - def title - 'Microsoft Teams notifications' - end - - def description - 'Send notifications about project events to Microsoft Teams.' - end - - def self.to_param - 'microsoft_teams' - end - - def help - '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>' - end - - def webhook_placeholder - 'https://outlook.office.com/webhook/…' - end - - def event_field(event) - end - - def default_channel_placeholder - end - - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" }, - { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } - ] - end - - private - - def notify(message, opts) - MicrosoftTeams::Notifier.new(webhook).ping( - title: message.project_name, - summary: message.summary, - activity: message.activity, - attachments: message.attachments - ) - end - - def custom_data(data) - super(data).merge(markdown: true) - end -end diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb deleted file mode 100644 index bd6344c6e1a..00000000000 --- a/app/models/project_services/mock_ci_service.rb +++ /dev/null @@ -1,90 +0,0 @@ -# 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 diff --git a/app/models/project_services/open_project_service.rb b/app/models/project_services/open_project_service.rb deleted file mode 100644 index a24fbc1611d..00000000000 --- a/app/models/project_services/open_project_service.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class OpenProjectService < IssueTrackerService - validates :url, public_url: true, presence: true, if: :activated? - validates :api_url, public_url: true, allow_blank: true, if: :activated? - validates :token, presence: true, if: :activated? - validates :project_identifier_code, presence: true, if: :activated? - - data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code - - def data_fields - open_project_tracker_data || self.build_open_project_tracker_data - end - - def self.to_param - 'open_project' - end -end diff --git a/app/models/project_services/open_project_tracker_data.rb b/app/models/project_services/open_project_tracker_data.rb deleted file mode 100644 index 20de60e40c1..00000000000 --- a/app/models/project_services/open_project_tracker_data.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class OpenProjectTrackerData < ApplicationRecord - include Services::DataFields - - # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8. - DEFAULT_CLOSED_STATUS_ID = "13" - - attr_encrypted :url, encryption_options - attr_encrypted :api_url, encryption_options - attr_encrypted :token, encryption_options - - def closed_status_id - super || DEFAULT_CLOSED_STATUS_ID - end -end diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb deleted file mode 100644 index f3ea8c64302..00000000000 --- a/app/models/project_services/packagist_service.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class PackagistService < Integration - prop_accessor :username, :token, :server - - validates :username, presence: true, if: :activated? - validates :token, presence: true, if: :activated? - - default_value_for :push_events, true - default_value_for :tag_push_events, true - - after_save :compose_service_hook, if: :activated? - - def title - 'Packagist' - end - - def description - s_('Integrations|Update your Packagist projects.') - end - - def self.to_param - 'packagist' - end - - def fields - [ - { type: 'text', name: 'username', placeholder: '', required: true }, - { type: 'text', name: 'token', placeholder: '', required: true }, - { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false } - ] - end - - def self.supported_events - %w(push merge_request tag_push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - service_hook.execute(data) - end - - def test(data) - begin - result = execute(data) - return { success: false, result: result[:message] } if result[:http_status] != 202 - rescue StandardError => error - return { success: false, result: error } - end - - { success: true, result: result[:message] } - end - - def compose_service_hook - hook = service_hook || build_service_hook - hook.url = hook_url - hook.save - end - - def hook_url - base_url = server.presence || 'https://packagist.org' - "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}" - end -end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb deleted file mode 100644 index 4603193ac8e..00000000000 --- a/app/models/project_services/pipelines_email_service.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -class PipelinesEmailService < Integration - include NotificationBranchSelection - - prop_accessor :recipients, :branches_to_be_notified - boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch - validates :recipients, presence: true, if: :validate_recipients? - - def initialize_properties - if properties.nil? - self.properties = {} - self.notify_only_broken_pipelines = true - self.branches_to_be_notified = "default" - elsif !self.notify_only_default_branch.nil? - # In older versions, there was only a boolean property named - # `notify_only_default_branch`. Now we have a string property named - # `branches_to_be_notified`. Instead of doing a background migration, we - # opted to set a value for the new property based on the old one, if - # users hasn't specified one already. When users edit the service and - # selects a value for this new property, it will override everything. - - self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all" - end - end - - def title - _('Pipeline status emails') - end - - def description - _('Email the pipeline status to a list of recipients.') - end - - def self.to_param - 'pipelines_email' - end - - def self.supported_events - %w[pipeline] - end - - def self.default_test_event - 'pipeline' - end - - def execute(data, force: false) - return unless supported_events.include?(data[:object_kind]) - return unless force || should_pipeline_be_notified?(data) - - all_recipients = retrieve_recipients(data) - - return unless all_recipients.any? - - pipeline_id = data[:object_attributes][:id] - PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients) - end - - def can_test? - project&.ci_pipelines&.any? - end - - def fields - [ - { type: 'textarea', - name: 'recipients', - help: _('Comma-separated list of email addresses.'), - required: true }, - { type: 'checkbox', - name: 'notify_only_broken_pipelines' }, - { type: 'select', - name: 'branches_to_be_notified', - choices: branch_choices } - ] - end - - def test(data) - result = execute(data, force: true) - - { success: true, result: result } - rescue StandardError => error - { success: false, result: error } - end - - def should_pipeline_be_notified?(data) - notify_for_branch?(data) && notify_for_pipeline?(data) - end - - def notify_for_pipeline?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end - end - - def retrieve_recipients(data) - recipients.to_s.split(/[,\r\n ]+/).reject(&:empty?) - end -end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb deleted file mode 100644 index 6e67984591d..00000000000 --- a/app/models/project_services/pivotaltracker_service.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -class PivotaltrackerService < Integration - API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits' - - prop_accessor :token, :restrict_to_branch - validates :token, presence: true, if: :activated? - - def title - 'PivotalTracker' - end - - def description - s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.') - end - - def self.to_param - 'pivotaltracker' - end - - def fields - [ - { - type: 'text', - name: 'token', - placeholder: s_('PivotalTrackerService|Pivotal Tracker API token.'), - required: true - }, - { - type: 'text', - name: 'restrict_to_branch', - placeholder: s_('PivotalTrackerService|Comma-separated list of branches which will be ' \ - 'automatically inspected. Leave blank to include all branches.') - } - ] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless allowed_branch?(data[:ref]) - - data[:commits].each do |commit| - message = { - 'source_commit' => { - 'commit_id' => commit[:id], - 'author' => commit[:author][:name], - 'url' => commit[:url], - 'message' => commit[:message] - } - } - Gitlab::HTTP.post( - API_ENDPOINT, - body: message.to_json, - headers: { - 'Content-Type' => 'application/json', - 'X-TrackerToken' => token - } - ) - end - end - - private - - def allowed_branch?(ref) - return true unless ref.present? && restrict_to_branch.present? - - branch = Gitlab::Git.ref_name(ref) - allowed_branches = restrict_to_branch.split(',').map(&:strip) - - branch.present? && allowed_branches.include?(branch) - end -end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index b8869547a37..a289c1c2afb 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -117,8 +117,8 @@ class PrometheusService < MonitoringService return false if template? return false unless project - project.all_clusters.enabled.eager_load(:application_prometheus).any? do |cluster| - cluster.application_prometheus&.available? + project.all_clusters.enabled.eager_load(:integration_prometheus).any? do |cluster| + cluster.integration_prometheus_available? end end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb deleted file mode 100644 index 89765fbdf41..00000000000 --- a/app/models/project_services/pushover_service.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -class PushoverService < Integration - BASE_URI = 'https://api.pushover.net/1' - - prop_accessor :api_key, :user_key, :device, :priority, :sound - validates :api_key, :user_key, :priority, presence: true, if: :activated? - - def title - 'Pushover' - end - - def description - s_('PushoverService|Get real-time notifications on your device.') - end - - def self.to_param - 'pushover' - end - - def fields - [ - { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true }, - { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true }, - { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') }, - { type: 'select', name: 'priority', required: true, choices: - [ - [s_('PushoverService|Lowest Priority'), -2], - [s_('PushoverService|Low Priority'), -1], - [s_('PushoverService|Normal Priority'), 0], - [s_('PushoverService|High Priority'), 1] - ], - default_choice: 0 }, - { type: 'select', name: 'sound', choices: - [ - ['Device default sound', nil], - ['Pushover (default)', 'pushover'], - %w(Bike bike), - %w(Bugle bugle), - ['Cash Register', 'cashregister'], - %w(Classical classical), - %w(Cosmic cosmic), - %w(Falling falling), - %w(Gamelan gamelan), - %w(Incoming incoming), - %w(Intermission intermission), - %w(Magic magic), - %w(Mechanical mechanical), - ['Piano Bar', 'pianobar'], - %w(Siren siren), - ['Space Alarm', 'spacealarm'], - ['Tug Boat', 'tugboat'], - ['Alien Alarm (long)', 'alien'], - ['Climb (long)', 'climb'], - ['Persistent (long)', 'persistent'], - ['Pushover Echo (long)', 'echo'], - ['Up Down (long)', 'updown'], - ['None (silent)', 'none'] - ] } - ] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - ref = Gitlab::Git.ref_name(data[:ref]) - before = data[:before] - after = data[:after] - - message = - if Gitlab::Git.blank_ref?(before) - s_("PushoverService|%{user_name} pushed new branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } - elsif Gitlab::Git.blank_ref?(after) - s_("PushoverService|%{user_name} deleted branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } - else - s_("PushoverService|%{user_name} push to branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } - end - - if data[:total_commits_count] > 0 - message = [message, s_("PushoverService|Total commits count: %{total_commits_count}") % { total_commits_count: data[:total_commits_count] }].join("\n") - end - - pushover_data = { - token: api_key, - user: user_key, - device: device, - priority: priority, - title: "#{project.full_name}", - message: message, - url: data[:project][:web_url], - url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name } - } - - # Sound parameter MUST NOT be sent to API if not selected - if sound - pushover_data[:sound] = sound - end - - Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data) - end -end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb deleted file mode 100644 index 7a0f500209c..00000000000 --- a/app/models/project_services/redmine_service.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class RedmineService < IssueTrackerService - include ActionView::Helpers::UrlHelper - validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - - def title - 'Redmine' - end - - def description - s_("IssueTracker|Use Redmine as this project's issue tracker.") - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer' - s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'redmine' - end -end diff --git a/app/models/project_services/slack_mattermost/notifier.rb b/app/models/project_services/slack_mattermost/notifier.rb deleted file mode 100644 index 1a78cea5933..00000000000 --- a/app/models/project_services/slack_mattermost/notifier.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module SlackMattermost - module Notifier - private - - def notify(message, opts) - # See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client - notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient)) - notifier.ping( - message.pretext, - attachments: message.attachments, - fallback: message.fallback - ) - end - - class HTTPClient - def self.post(uri, params = {}) - params.delete(:http_options) # these are internal to the client and we do not want them - Gitlab::HTTP.post(uri, body: params) - end - end - end -end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb deleted file mode 100644 index 92a46f8d01f..00000000000 --- a/app/models/project_services/slack_service.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -class SlackService < ChatNotificationService - include SlackMattermost::Notifier - extend ::Gitlab::Utils::Override - - SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[ - push issue confidential_issue merge_request note confidential_note - tag_push wiki_page deployment - ].freeze - - prop_accessor EVENT_CHANNEL['alert'] - - def title - 'Slack notifications' - end - - def description - 'Send notifications about project events to Slack.' - end - - def self.to_param - 'slack' - end - - def default_channel_placeholder - _('general, development') - end - - def webhook_placeholder - 'https://hooks.slack.com/services/…' - end - - def supported_events - additional = [] - additional << 'alert' - - super + additional - end - - def get_message(object_kind, data) - return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' - - super - end - - override :log_usage - def log_usage(event, user_id) - return unless user_id - - return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event) - - key = "i_ecosystem_slack_service_#{event}_notification" - - Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id) - end -end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb deleted file mode 100644 index 548f3623504..00000000000 --- a/app/models/project_services/slack_slash_commands_service.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -class SlackSlashCommandsService < SlashCommandsService - include Ci::TriggersHelper - - def title - 'Slack slash commands' - end - - def description - "Perform common operations in Slack" - end - - def self.to_param - 'slack_slash_commands' - end - - def trigger(params) - # Format messages to be Slack-compatible - super.tap do |result| - result[:text] = format(result[:text]) if result.is_a?(Hash) - end - end - - def chat_responder - ::Gitlab::Chat::Responder::Slack - end - - private - - def format(text) - Slack::Messenger::Util::LinkFormatter.format(text) if text - end -end diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb deleted file mode 100644 index 37d16737052..00000000000 --- a/app/models/project_services/slash_commands_service.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -# Base class for Chat services -# This class is not meant to be used directly, but only to inherrit from. -class SlashCommandsService < Integration - default_value_for :category, 'chat' - - prop_accessor :token - - has_many :chat_names, foreign_key: :service_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.secure_compare(token, self.token) - end - - def self.supported_events - %w() - end - - def can_test? - false - end - - def fields - [ - { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' } - ] - end - - def trigger(params) - return unless valid_token?(params[:token]) - - chat_user = find_chat_user(params) - user = chat_user&.user - - if user - unless user.can?(:use_slash_commands) - return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated? - - return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project) - end - - Gitlab::SlashCommands::Command.new(project, chat_user, params).execute - else - url = authorize_chat_name_url(params) - Gitlab::SlashCommands::Presenters::Access.new(url).authorize - end - end - - private - - # rubocop: disable CodeReuse/ServiceClass - def find_chat_user(params) - ChatNames::FindUserService.new(self, params).execute - end - # rubocop: enable CodeReuse/ServiceClass - - # rubocop: disable CodeReuse/ServiceClass - def authorize_chat_name_url(params) - ChatNames::AuthorizeUserService.new(self, params).execute - end - # rubocop: enable CodeReuse/ServiceClass -end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb deleted file mode 100644 index 6fc24a4778c..00000000000 --- a/app/models/project_services/teamcity_service.rb +++ /dev/null @@ -1,189 +0,0 @@ -# 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 diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb deleted file mode 100644 index 5f43388e1c9..00000000000 --- a/app/models/project_services/unify_circuit_service.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -class UnifyCircuitService < ChatNotificationService - def title - 'Unify Circuit' - end - - def description - s_('Integrations|Send notifications about project events to Unify Circuit.') - end - - def self.to_param - 'unify_circuit' - end - - def help - 'This service sends notifications about projects events to a Unify Circuit conversation.<br /> - To set up this service: - <ol> - <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li> - <li>Paste the <strong>Webhook URL</strong> into the field below.</li> - <li>Select events below to enable notifications.</li> - </ol>' - end - - def event_field(event) - end - - def default_channel_placeholder - end - - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } - ] - end - - private - - def notify(message, opts) - response = Gitlab::HTTP.post(webhook, body: { - subject: message.project_name, - text: message.summary, - markdown: true - }.to_json) - - response if response.success? - end - - def custom_data(data) - super(data).merge(markdown: true) - end -end diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb deleted file mode 100644 index 3d92d3bb85e..00000000000 --- a/app/models/project_services/webex_teams_service.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -class WebexTeamsService < ChatNotificationService - include ActionView::Helpers::UrlHelper - - def title - s_("WebexTeamsService|Webex Teams") - end - - def description - s_("WebexTeamsService|Send notifications about project events to Webex Teams.") - end - - def self.to_param - 'webex_teams' - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' - s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } - end - - def event_field(event) - end - - def default_channel_placeholder - end - - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } - ] - end - - private - - def notify(message, opts) - header = { 'Content-Type' => 'application/json' } - response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json) - - response if response.success? - end - - def custom_data(data) - super(data).merge(markdown: true) - end -end diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb deleted file mode 100644 index 9760a22a872..00000000000 --- a/app/models/project_services/youtrack_service.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -class YoutrackService < IssueTrackerService - include ActionView::Helpers::UrlHelper - - validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? - - # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 - def self.reference_pattern(only_long: false) - if only_long - /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/ - else - /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})/ - end - end - - def title - 'YouTrack' - end - - def description - s_("IssueTracker|Use YouTrack as this project's issue tracker.") - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer' - s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'youtrack' - end - - def fields - [ - { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true }, - { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true } - ] - end -end |