Read about our upcoming Code of Conduct on this issue

service.rb 8.55 KB
Newer Older
1
2
# frozen_string_literal: true

3
4
# To add new service you should build a class inherited from Service
# and implement a set of methods
5
class Service < ActiveRecord::Base
6
  include Sortable
James Lopez's avatar
James Lopez committed
7
  include Importable
8
  include ProjectServicesLoggable
James Lopez's avatar
James Lopez committed
9

10
  serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
Drew Blessing's avatar
Drew Blessing committed
11

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
12
  default_value_for :active, false
13
14
  default_value_for :push_events, true
  default_value_for :issues_events, true
15
  default_value_for :confidential_issues_events, true
16
  default_value_for :commit_events, true
17
18
  default_value_for :merge_requests_events, true
  default_value_for :tag_push_events, true
19
  default_value_for :note_events, true
20
  default_value_for :confidential_note_events, true
21
  default_value_for :job_events, true
22
  default_value_for :pipeline_events, true
23
  default_value_for :wiki_page_events, true
24
25

  after_initialize :initialize_properties
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
26

27
  after_commit :reset_updated_properties
28
  after_commit :cache_project_has_external_issue_tracker
29
  after_commit :cache_project_has_external_wiki
30

31
  belongs_to :project, inverse_of: :services
32
33
  has_one :service_hook

mhasbini's avatar
mhasbini committed
34
  validates :project_id, presence: true, unless: proc { |service| service.template? }
Tiago Botelho's avatar
Tiago Botelho committed
35
  validates :type, presence: true
36

Z.J. van de Weg's avatar
Z.J. van de Weg committed
37
  scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
38
  scope :issue_trackers, -> { where(category: 'issue_tracker') }
39
  scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
40
41
  scope :active, -> { where(active: true) }
  scope :without_defaults, -> { where(default: false) }
42

43
44
45
  scope :push_hooks, -> { where(push_events: true, active: true) }
  scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
  scope :issue_hooks, -> { where(issues_events: true, active: true) }
46
  scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
47
  scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
48
  scope :note_hooks, -> { where(note_events: true, active: true) }
49
  scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
50
  scope :job_hooks, -> { where(job_events: true, active: true) }
51
  scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
52
  scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
53
  scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
54
  scope :deployment, -> { where(category: 'deployment') }
55

56
57
  default_value_for :category, 'common'

58
59
60
  def activated?
    active
  end
61

62
63
64
65
66
67
68
69
  def show_active_box?
    true
  end

  def editable?
    true
  end

70
71
72
73
  def template?
    template
  end

74
  def category
75
    read_attribute(:category).to_sym
76
77
  end

78
79
80
81
  def initialize_properties
    self.properties = {} if properties.nil?
  end

82
  def title
83
    # implement inside child
84
85
86
  end

  def description
87
    # implement inside child
88
89
  end

90
91
92
93
  def help
    # implement inside child
  end

94
  def to_param
95
    # implement inside child
96
    self.class.to_param
97
98
  end

Tiago Botelho's avatar
Tiago Botelho committed
99
100
101
102
  def self.to_param
    raise NotImplementedError
  end

103
  def fields
104
    # implement inside child
105
106
    []
  end
107

108
  def test_data(project, user)
109
    Gitlab::DataBuilder::Push.build_sample(project, user)
110
111
  end

112
113
114
115
  def event_channel_names
    []
  end

116
  def event_names
117
    self.class.event_names
118
119
  end

Tiago Botelho's avatar
Tiago Botelho committed
120
121
122
123
  def self.event_names
    self.supported_events.map { |event| "#{event}_events" }
  end

Felipe Artur's avatar
Felipe Artur committed
124
125
126
127
  def event_field(event)
    nil
  end

Stan Hu's avatar
Stan Hu committed
128
129
130
131
132
  def api_field_names
    fields.map { |field| field[:name] }
      .reject { |field_name| field_name =~ /(password|token|key)/ }
  end

Felipe Artur's avatar
Felipe Artur committed
133
134
135
136
  def global_fields
    fields
  end

137
138
139
140
141
142
143
144
145
146
147
  def configurable_events
    events = self.class.supported_events

    # No need to disable individual triggers when there is only one
    if events.count == 1
      []
    else
      events
    end
  end

148
  def supported_events
149
    self.class.supported_events
150
151
  end

Tiago Botelho's avatar
Tiago Botelho committed
152
153
154
155
  def self.supported_events
    %w(push tag_push issue confidential_issue merge_request wiki_page)
  end

156
  def execute(data)
157
158
    # implement inside child
  end
159

160
161
162
163
164
165
  def test(data)
    # default implementation
    result = execute(data)
    { success: result.present?, result: result }
  end

166
  def can_test?
167
    true
168
  end
Drew Blessing's avatar
Drew Blessing committed
169
170
171

  # Provide convenient accessor methods
  # for each serialized property.
172
  # Also keep track of updated properties in a similar way as ActiveModel::Dirty
Drew Blessing's avatar
Drew Blessing committed
173
174
175
  def self.prop_accessor(*args)
    args.each do |arg|
      class_eval %{
176
177
178
179
        unless method_defined?(arg)
          def #{arg}
            properties['#{arg}']
          end
Drew Blessing's avatar
Drew Blessing committed
180
181
182
        end

        def #{arg}=(value)
183
          self.properties ||= {}
184
          updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
Drew Blessing's avatar
Drew Blessing committed
185
186
          self.properties['#{arg}'] = value
        end
187
188
189
190
191
192
193
194
195
196
197
198

        def #{arg}_changed?
          #{arg}_touched? && #{arg} != #{arg}_was
        end

        def #{arg}_touched?
          updated_properties.include?('#{arg}')
        end

        def #{arg}_was
          updated_properties['#{arg}']
        end
Drew Blessing's avatar
Drew Blessing committed
199
200
201
      }
    end
  end
202

203
204
205
206
207
208
209
210
  # Provide convenient boolean accessor methods
  # for each serialized property.
  # Also keep track of updated properties in a similar way as ActiveModel::Dirty
  def self.boolean_accessor(*args)
    self.prop_accessor(*args)

    args.each do |arg|
      class_eval %{
211
        def #{arg}?
212
          # '!!' is used because nil or empty string is converted to nil
Jasper Maes's avatar
Jasper Maes committed
213
          !!ActiveRecord::Type::Boolean.new.cast(#{arg})
214
215
        end
      }
216
217
218
    end
  end

219
220
  # Returns a hash of the properties that have been assigned a new value since last save,
  # indicating their original values (attr => original value).
221
  # ActiveRecord does not provide a mechanism to track changes in serialized keys,
222
223
224
225
226
  # so we need a specific implementation for service properties.
  # This allows to track changes to properties set with the accessor methods,
  # but not direct manipulation of properties hash.
  def updated_properties
    @updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new
227
228
  end

229
230
231
  def reset_updated_properties
    @updated_properties = nil
  end
232

233
  def async_execute(data)
234
    return unless supported_events.include?(data[:object_kind])
235

236
    ProjectServiceWorker.perform_async(id, data)
237
  end
238
239
240
241
242

  def issue_tracker?
    self.category == :issue_tracker
  end

243
  def self.available_services_names
Eric Eastwood's avatar
Eric Eastwood committed
244
    service_names = %w[
245
      asana
246
247
      assembla
      bamboo
248
      buildkite
249
      bugzilla
250
251
      campfire
      custom_issue_tracker
blackst0ne's avatar
blackst0ne committed
252
      discord
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
253
      drone_ci
254
      emails_on_push
255
256
      external_wiki
      flowdock
257
      hangouts_chat
258
      irker
259
      jira
260
      kubernetes
261
      mattermost_slash_commands
262
      mattermost
Matt Coleman's avatar
Matt Coleman committed
263
      packagist
264
      pipelines_email
265
      pivotaltracker
266
      prometheus
267
      pushover
268
      redmine
Yauhen Kotau's avatar
Yauhen Kotau committed
269
      youtrack
270
      slack_slash_commands
271
      slack
272
      teamcity
273
      microsoft_teams
Lin Jen-Shin's avatar
Lin Jen-Shin committed
274
    ]
275

276
277
278
    if Rails.env.development?
      service_names += %w[mock_ci mock_deployment mock_monitoring]
    end
Eric Eastwood's avatar
Eric Eastwood committed
279
280

    service_names.sort_by(&:downcase)
281
282
  end

283
  def self.build_from_template(project_id, template)
284
285
286
    service = template.dup
    service.template = false
    service.project_id = project_id
287
    service.active = false if service.active? && !service.valid?
288
    service
289
  end
290

291
292
293
294
295
296
297
298
  def deprecated?
    false
  end

  def deprecation_message
    nil
  end

299
300
301
302
  def self.find_by_template
    find_by(template: true)
  end

303
304
305
306
307
308
309
  private

  def cache_project_has_external_issue_tracker
    if project && !project.destroyed?
      project.cache_has_external_issue_tracker
    end
  end
310
311
312
313
314
315

  def cache_project_has_external_wiki
    if project && !project.destroyed?
      project.cache_has_external_wiki
    end
  end
James Lopez's avatar
James Lopez committed
316

Felipe Artur's avatar
Felipe Artur committed
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
  def self.event_description(event)
    case event
    when "push", "push_events"
      "Event will be triggered by a push to the repository"
    when "tag_push", "tag_push_events"
      "Event will be triggered when a new tag is pushed to the repository"
    when "note", "note_events"
      "Event will be triggered when someone adds a comment"
    when "issue", "issue_events"
      "Event will be triggered when an issue is created/updated/closed"
    when "confidential_issue", "confidential_issue_events"
      "Event will be triggered when a confidential issue is created/updated/closed"
    when "merge_request", "merge_request_events"
      "Event will be triggered when a merge request is created/updated/merged"
    when "pipeline", "pipeline_events"
      "Event will be triggered when a pipeline status changes"
    when "wiki_page", "wiki_page_events"
      "Event will be triggered when a wiki page is created/updated"
    when "commit", "commit_events"
      "Event will be triggered when a commit is created/updated"
    end
  end

James Lopez's avatar
James Lopez committed
340
341
342
  def valid_recipients?
    activated? && !importing?
  end
343
end