Read about our upcoming Code of Conduct on this issue

service.rb 8.6 KB
Newer Older
1
2
# To add new service you should build a class inherited from Service
# and implement a set of methods
3
class Service < ActiveRecord::Base
4
  include Sortable
James Lopez's avatar
James Lopez committed
5
6
  include Importable

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

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

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

24
  after_commit :reset_updated_properties
25
  after_commit :cache_project_has_external_issue_tracker
26
  after_commit :cache_project_has_external_wiki
27

28
  belongs_to :project, inverse_of: :services
29
30
  has_one :service_hook

mhasbini's avatar
mhasbini committed
31
  validates :project_id, presence: true, unless: proc { |service| service.template? }
Tiago Botelho's avatar
Tiago Botelho committed
32
  validates :type, presence: true
33

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

40
41
42
  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) }
43
  scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
44
  scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
45
  scope :note_hooks, -> { where(note_events: true, active: true) }
46
  scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
47
  scope :job_hooks, -> { where(job_events: true, active: true) }
48
  scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
49
  scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
50
  scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
51
  scope :deployment, -> { where(category: 'deployment') }
52

53
54
  default_value_for :category, 'common'

55
56
57
  def activated?
    active
  end
58

59
60
61
62
63
64
65
66
  def show_active_box?
    true
  end

  def editable?
    true
  end

67
68
69
70
  def template?
    template
  end

71
  def category
72
    read_attribute(:category).to_sym
73
74
  end

75
76
77
78
  def initialize_properties
    self.properties = {} if properties.nil?
  end

79
  def title
80
    # implement inside child
81
82
83
  end

  def description
84
    # implement inside child
85
86
  end

87
88
89
90
  def help
    # implement inside child
  end

91
  def to_param
92
    # implement inside child
93
    self.class.to_param
94
95
  end

Tiago Botelho's avatar
Tiago Botelho committed
96
97
98
99
  def self.to_param
    raise NotImplementedError
  end

100
  def fields
101
    # implement inside child
102
103
    []
  end
104

105
  def test_data(project, user)
106
    Gitlab::DataBuilder::Push.build_sample(project, user)
107
108
  end

109
110
111
112
  def event_channel_names
    []
  end

113
  def event_names
114
    self.class.event_names
115
116
  end

Tiago Botelho's avatar
Tiago Botelho committed
117
118
119
120
  def self.event_names
    self.supported_events.map { |event| "#{event}_events" }
  end

Felipe Artur's avatar
Felipe Artur committed
121
122
123
124
  def event_field(event)
    nil
  end

Stan Hu's avatar
Stan Hu committed
125
126
127
128
129
  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
130
131
132
133
  def global_fields
    fields
  end

134
135
136
137
138
139
140
141
142
143
144
  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

145
  def supported_events
146
    self.class.supported_events
147
148
  end

Tiago Botelho's avatar
Tiago Botelho committed
149
150
151
152
  def self.supported_events
    %w(push tag_push issue confidential_issue merge_request wiki_page)
  end

153
  def execute(data)
154
155
    # implement inside child
  end
156

157
158
159
160
161
162
  def test(data)
    # default implementation
    result = execute(data)
    { success: result.present?, result: result }
  end

163
  def can_test?
164
    true
165
  end
Drew Blessing's avatar
Drew Blessing committed
166
167
168

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

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

        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
196
197
198
      }
    end
  end
199

200
201
202
203
204
205
206
207
  # 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 %{
208
        def #{arg}?
209
          # '!!' is used because nil or empty string is converted to nil
210
          if Gitlab.rails5?
211
            !!ActiveRecord::Type::Boolean.new.cast(#{arg})
212
          else
213
            !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg})
214
          end
215
216
        end
      }
217
218
219
    end
  end

220
221
  # Returns a hash of the properties that have been assigned a new value since last save,
  # indicating their original values (attr => original value).
222
  # ActiveRecord does not provide a mechanism to track changes in serialized keys,
223
224
225
226
227
  # 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
228
229
  end

230
231
232
  def reset_updated_properties
    @updated_properties = nil
  end
233

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

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

  def issue_tracker?
    self.category == :issue_tracker
  end

244
  def self.available_services_names
Eric Eastwood's avatar
Eric Eastwood committed
245
    service_names = %w[
246
      asana
247
248
      assembla
      bamboo
249
      buildkite
250
      bugzilla
251
252
      campfire
      custom_issue_tracker
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
253
      drone_ci
254
      emails_on_push
255
256
      external_wiki
      flowdock
257
      hangouts_chat
258
259
      hipchat
      irker
260
      jira
261
      kubernetes
262
      mattermost_slash_commands
263
      mattermost
Matt Coleman's avatar
Matt Coleman committed
264
      packagist
265
      pipelines_email
266
      pivotaltracker
267
      prometheus
268
      pushover
269
      redmine
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