Commit da5fc736 authored by Felipe Artur's avatar Felipe Artur Committed by Douglas Barbosa Alexandre

Change SlackService to SlackNotificationsService

parent 4388ee2a
......@@ -89,8 +89,8 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :mattermost_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :mattermost_notification_service, dependent: :destroy
has_one :slack_notification_service, dependent: :destroy
has_one :jenkins_service, dependent: :destroy
has_one :jenkins_deprecated_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
......
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
class ChatNotificationService < Service
include ChatMessage
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
# Custom serialized properties initialization
self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
def can_test?
valid?
end
def supported_events
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless webhook.present?
object_kind = data[:object_kind]
data = data.merge(
project_url: project_url,
project_name: project_name
)
# 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
opt = {}
opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
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
raise NotImplementedError
end
private
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
PushMessage.new(data)
when "issue"
IssueMessage.new(data) unless is_update?(data)
when "merge_request"
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
end
end
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
end
def build_event_channels
supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
end
end
def event_channel_name(event)
"#{event}_channel"
end
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
def project_url
project.web_url
end
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
def should_build_be_notified?(data)
case data[:commit][:status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end
# Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from.
# This class is not meant to be used directly, but only to inherit from.
class ChatService < Service
include ChatMessage
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
# Custom serialized properties initialization
self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
has_many :chat_names, foreign_key: :service_id
def can_test?
valid?
def valid_token?(token)
self.respond_to?(:token) &&
self.token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless webhook.present?
object_kind = data[:object_kind]
data = data.merge(
project_url: project_url,
project_name: project_name
)
# 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
opt = {}
opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
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
def trigger(params)
raise NotImplementedError
end
private
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
PushMessage.new(data)
when "issue"
IssueMessage.new(data) unless is_update?(data)
when "merge_request"
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
end
end
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
end
def build_event_channels
supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
end
end
def event_channel_name(event)
"#{event}_channel"
end
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
def project_url
project.web_url
end
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
def should_build_be_notified?(data)
case data[:commit][:status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end
class MattermostService < ChatService
class MattermostNotificationService < ChatNotificationService
def title
'Mattermost notifications'
end
......@@ -8,7 +8,7 @@ class MattermostService < ChatService
end
def to_param
'mattermost'
'mattermost_notification'
end
def help
......
class MattermostSlashCommandsService < Service
class MattermostSlashCommandsService < ChatService
include TriggersHelper
prop_accessor :token
def valid_token?(token)
self.respond_to?(:token) &&
self.token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
[]
end
def can_test?
false
end
......
class SlackService < ChatService
class SlackNotificationService < ChatNotificationService
def title
'Slack notifications'
end
......@@ -8,7 +8,7 @@ class SlackService < ChatService
end
def to_param
'slack'
'slack_notification'
end
def help
......
......@@ -221,8 +221,8 @@ class Service < ActiveRecord::Base
pivotaltracker
pushover
redmine
mattermost
slack
mattermost_notification
slack_notification
teamcity
]
end
......
# rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change
SlackService.all.each do |slack_service|
SlackNotificationService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain']
......
class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
def up
execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
end
def down
execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161202152035) do
ActiveRecord::Schema.define(version: 20161213172958) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
module API
# Projects API
class Services < Grape::API
services = {
'asana' => [
{
required: true,
name: :api_key,
type: String,
desc: 'User API token'
},
{
required: false,
name: :restrict_to_branch,
type: String,
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
}
],
'assembla' => [
{
required: true,
name: :token,
type: String,
desc: 'The authentication token'
},
{
required: false,
name: :subdomain,
type: String,
desc: 'Subdomain setting'
}
],
'bamboo' => [
{
required: true,
name: :bamboo_url,
type: String,
desc: 'Bamboo root URL like https://bamboo.example.com'
},
{
required: true,
name: :build_key,
type: String,
desc: 'Bamboo build plan key like'
},
{
required: true,
name: :username,
type: String,
desc: 'A user with API access, if applicable'
},
{
required: true,
name: :password,
type: String,
desc: 'Passord of the user'
}
],
'bugzilla' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'New issue URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'Issues URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'buildkite' => [
{
required: true,
name: :token,
type: String,
desc: 'Buildkite project GitLab token'
},
{
required: true,
name: :project_url,
type: String,
desc: 'The buildkite project URL'
},
{
required: false,
name: :enable_ssl_verification,
type: Boolean,
desc: 'Enable SSL verification for communication'
}
],
'builds-email' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :add_pusher,
type: Boolean,
desc: 'Add pusher to recipients list'
},
{
required: false,
name: :notify_only_broken_builds,
type: Boolean,
desc: 'Notify only broken builds'
}
],
'campfire' => [
{
required: true,
name: :token,
type: String,
desc: 'Campfire token'
},
{
required: false,
name: :subdomain,
type: String,
desc: 'Campfire subdomain'
},
{
required: false,
name: :room,
type: String,
desc: 'Campfire room'
},
],
'custom-issue-tracker' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'New issue URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'Issues URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'drone-ci' => [
{
required: true,
name: :token,
type: String,
desc: 'Drone CI token'
},
{
required: true,
name: :drone_url,
type: String,
desc: 'Drone CI URL'
},
{
required: false,
name: :enable_ssl_verification,
type: Boolean,
desc: 'Enable SSL verification for communication'
}
],
'emails-on-push' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :disable_diffs,
type: Boolean,
desc: 'Disable code diffs'
},
{
required: false,
name: :send_from_committer_email,
type: Boolean,
desc: 'Send from committer'
}
],
'external-wiki' => [
{
required: true,
name: :external_wiki_url,
type: String,
desc: 'The URL of the external Wiki'
}
],
'flowdock' => [
{
required: true,
name: :token,
type: String,
desc: 'Flowdock token'
}
],
'gemnasium' => [
{
required: true,
name: :api_key,
type: String,
desc: 'Your personal API key on gemnasium.com'
},
{
required: true,
name: :token,
type: String,
desc: "The project's slug on gemnasium.com"
}
],
'hipchat' => [
{
required: true,
name: :token,
type: String,
desc: 'The room token'
},
{
required: false,
name: :room,
type: String,
desc: 'The room name or ID'
},
{
required: false,
name: :color,
type: String,
desc: 'The room color'
},
{
required: false,
name: :notify,
type: Boolean,
desc: 'Enable notifications'
},
{
required: false,
name: :api_version,
type: String,
desc: 'Leave blank for default (v2)'
},
{
required: false,
name: :server,
type: String,
desc: 'Leave blank for default. https://hipchat.example.com'
}
],
'irker' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Recipients/channels separated by whitespaces'
},
{
required: false,
name: :default_irc_uri,
type: String,
desc: 'Default: irc://irc.network.net:6697'
},
{
required: false,
name: :server_host,
type: String,
desc: 'Server host. Default localhost'
},
{
required: false,
name: :server_port,
type: Integer,
desc: 'Server port. Default 6659'
},
{
required: false,
name: :colorize_messages,
type: Boolean,
desc: 'Colorize messages'
}
],
'jira' => [
{
required: true,
name: :url,
type: String,
desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
},
{
required: true,
name: :project_key,
type: String,
desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
},
{
required: false,
name: :username,
type: String,
desc: 'The username of the user created to be used with GitLab/JIRA'
},
{
required: false,
name: :password,
type: String,
desc: 'The password of the user created to be used with GitLab/JIRA'
},
{
required: false,
name: :jira_issue_transition_id,
type: Integer,
desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
}
],
'kubernetes' => [
{
required: true,
name: :namespace,
type: String,
desc: 'The Kubernetes namespace to use'
},
{
required: true,
name: :api_url,
type: String,
desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
},
{
required: true,
name: :token,
type: String,
desc: 'The service token to authenticate against the Kubernetes cluster with'
},
{
required: false,
name: :ca_pem,
type: String,
desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
},
],
'mattermost-slash-commands' => [
{
required: true,
name: :token,
type: String,
desc: 'The Mattermost token'
}
],
'pipelines-email' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :notify_only_broken_builds,
type: Boolean,
desc: 'Notify only broken builds'
}
],
'pivotaltracker' => [
{
required: true,
name: :token,
type: String,
desc: 'The Pivotaltracker token'
},
{
required: false,
name: :restrict_to_branch,
type: String,
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
],
'pushover' => [
{
required: true,
name: :api_key,
type: String,
desc: 'The application key'
},
{
required: true,
name: :user_key,
type: String,
desc: 'The user key'
},
{
required: true,
name: :priority,
type: String,
desc: 'The priority'
},
{
required: true,
name: :device,
type: String,
desc: 'Leave blank for all active devices'
},
{
required: true,
name: :sound,
type: String,
desc: 'The sound of the notification'
}
],
'redmine' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'The new issue URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'The project URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'The issues URL'
},
{
required: false,
name: :description,
type: String,
desc: 'The description of the tracker'
}
],
'slack-notification' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
},
{
required: false,
name: :new_issue_url,
type: String,
desc: 'The user name'
},
{
required: false,
name: :channel,
type: String,
desc: 'The channel name'
}
],
'mattermost-notification' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
}
],
'teamcity' => [
{
required: true,
name: :teamcity_url,
type: String,
desc: 'TeamCity root URL like https://teamcity.example.com'
},
{
required: true,
name: :build_type,
type: String,
desc: 'Build configuration ID'
},
{
required: true,
name: :username,
type: String,
desc: 'A user with permissions to trigger a manual build'
},
{
required: true,
name: :password,
type: String,
desc: 'The password of the user'
}
]
}.freeze
trigger_services = {
'mattermost-slash-commands' => [
{
name: :token,
type: String,
desc: 'The Mattermost token'
}
]
}.freeze
resource :projects do
before { authenticate! }
before { authorize_admin_project }
......
......@@ -2,8 +2,8 @@ require 'spec_helper'
feature 'Projects > Slack service > Setup events', feature: true do
let(:user) { create(:user) }
let(:service) { SlackService.new }
let(:project) { create(:project, slack_service: service) }
let(:service) { SlackNotificationService.new }
let(:project) { create(:project, slack_notification_service: service) }
background do
service.fields
......
......@@ -145,8 +145,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
- slack_service
- mattermost_service
- slack_notification_service
- mattermost_notification_service
- buildkite_service
- bamboo_service
- teamcity_service
......
require 'spec_helper'
describe ChatNotificationService, models: true do
describe "Associations" do
before do
allow(subject).to receive(:activated?).and_return(true)
end
it { is_expected.to validate_presence_of :webhook }
end
end
......@@ -2,7 +2,14 @@ require 'spec_helper'
describe ChatService, models: true do
describe "Associations" do
before { allow(subject).to receive(:activated?).and_return(true) }
it { is_expected.to validate_presence_of :webhook }
it { is_expected.to have_many :chat_names }
end
describe '#valid_token?' do
subject { described_class.new }
it 'is false as it has no token' do
expect(subject.valid_token?('wer')).to be_falsey
end
end
end
require 'spec_helper'
describe SlackService, models: true do
describe MattermostNotificationService, models: true do
it_behaves_like "slack or mattermost"
end
require 'spec_helper'
describe MattermostService, models: true do
describe SlackNotificationService, models: true do
it_behaves_like "slack or mattermost"
end
......@@ -22,8 +22,8 @@ describe Project, models: true do
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
it { is_expected.to have_many(:chat_services) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }
......
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