Commit eaf34116 authored by Arturo Herrero's avatar Arturo Herrero

Add instance column to services table

This is needed to support instance-level integrations alongside service
templates.

We want to make sure that we always have the correct state in the
database. This commit is adding the following validations:
- Validates presence of project_id if not instance
- Validates absence of project_id if instance
- Validates only one instance-level service per type
- Validates service is instance or template
parent f5c7d8eb
...@@ -32,9 +32,12 @@ class Service < ApplicationRecord ...@@ -32,9 +32,12 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: -> { template? } validates :project_id, presence: true, unless: -> { template? || instance? }
validates :project_id, absence: true, if: -> { instance? }
validates :type, presence: true validates :type, presence: true
validates :template, uniqueness: { scope: :type }, if: -> { template? } validates :template, uniqueness: { scope: :type }, if: -> { template? }
validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
validate :validate_is_instance_or_template
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
...@@ -326,6 +329,10 @@ class Service < ApplicationRecord ...@@ -326,6 +329,10 @@ class Service < ApplicationRecord
private private
def validate_is_instance_or_template
errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
end
def cache_project_has_external_issue_tracker def cache_project_has_external_issue_tracker
if project && !project.destroyed? if project && !project.destroyed?
project.cache_has_external_issue_tracker project.cache_has_external_issue_tracker
......
---
title: Add instance column to services table
merge_request: 25714
author:
type: other
# frozen_string_literal: true
class AddInstanceToServices < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :instance, :boolean, default: false)
end
def down
remove_column(:services, :instance)
end
end
# frozen_string_literal: true
class AddIndexToServiceUniqueInstancePerType < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:services, [:type, :instance], unique: true, where: 'instance IS TRUE')
end
def down
remove_concurrent_index(:services, [:type, :instance])
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_03_09_195710) do ActiveRecord::Schema.define(version: 2020_03_10_135823) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -3939,8 +3939,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do ...@@ -3939,8 +3939,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do
t.string "description", limit: 500 t.string "description", limit: 500
t.boolean "comment_on_event_enabled", default: true, null: false t.boolean "comment_on_event_enabled", default: true, null: false
t.boolean "template", default: false t.boolean "template", default: false
t.boolean "instance", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id" t.index ["project_id"], name: "index_services_on_project_id"
t.index ["template"], name: "index_services_on_template" t.index ["template"], name: "index_services_on_template"
t.index ["type", "instance"], name: "index_services_on_type_and_instance", unique: true, where: "(instance IS TRUE)"
t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)" t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)"
t.index ["type"], name: "index_services_on_type" t.index ["type"], name: "index_services_on_type"
end end
......
...@@ -248,6 +248,7 @@ excluded_attributes: ...@@ -248,6 +248,7 @@ excluded_attributes:
- :token_encrypted - :token_encrypted
services: services:
- :template - :template
- :instance
error_tracking_setting: error_tracking_setting:
- :encrypted_token - :encrypted_token
- :encrypted_token_iv - :encrypted_token_iv
......
...@@ -4,6 +4,11 @@ FactoryBot.define do ...@@ -4,6 +4,11 @@ FactoryBot.define do
factory :service do factory :service do
project project
type { 'Service' } type { 'Service' }
trait :instance do
project { nil }
instance { true }
end
end end
factory :custom_issue_tracker_service, class: 'CustomIssueTrackerService' do factory :custom_issue_tracker_service, class: 'CustomIssueTrackerService' do
......
...@@ -111,6 +111,28 @@ ...@@ -111,6 +111,28 @@
"active": false, "active": false,
"properties": {}, "properties": {},
"template": true, "template": true,
"instance": false,
"push_events": true,
"issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
"job_events": true,
"type": "TeamcityService",
"category": "ci",
"default": false,
"wiki_page_events": true
},
{
"id": 101,
"title": "JetBrains TeamCity CI",
"project_id": 5,
"created_at": "2016-06-14T15:01:51.315Z",
"updated_at": "2016-06-14T15:01:51.315Z",
"active": false,
"properties": {},
"template": false,
"instance": true,
"push_events": true, "push_events": true,
"issues_events": true, "issues_events": true,
"merge_requests_events": true, "merge_requests_events": true,
......
...@@ -703,6 +703,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do ...@@ -703,6 +703,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(project.services.where(template: true).count).to eq(0) expect(project.services.where(template: true).count).to eq(0)
end end
it 'does not import any instance services' do
expect(restored_project_json).to eq(true)
expect(project.services.where(instance: true).count).to eq(0)
end
it 'imports labels' do it 'imports labels' do
create(:group_label, name: 'Another label', group: project.group) create(:group_label, name: 'Another label', group: project.group)
......
...@@ -459,6 +459,7 @@ Service: ...@@ -459,6 +459,7 @@ Service:
- active - active
- properties - properties
- template - template
- instance
- push_events - push_events
- issues_events - issues_events
- commit_events - commit_events
......
...@@ -18,6 +18,20 @@ describe Service do ...@@ -18,6 +18,20 @@ describe Service do
expect(build(:service, project_id: nil, template: false)).to be_invalid expect(build(:service, project_id: nil, template: false)).to be_invalid
end end
it 'validates presence of project_id if not instance', :aggregate_failures do
expect(build(:service, project_id: nil, instance: true)).to be_valid
expect(build(:service, project_id: nil, instance: false)).to be_invalid
end
it 'validates absence of project_id if instance', :aggregate_failures do
expect(build(:service, project_id: nil, instance: true)).to be_valid
expect(build(:service, instance: true)).to be_invalid
end
it 'validates service is template or instance' do
expect(build(:service, project_id: nil, template: true, instance: true)).to be_invalid
end
context 'with an existing service template' do context 'with an existing service template' do
before do before do
create(:service, type: 'Service', template: true) create(:service, type: 'Service', template: true)
...@@ -27,6 +41,16 @@ describe Service do ...@@ -27,6 +41,16 @@ describe Service do
expect(build(:service, type: 'Service', template: true)).to be_invalid expect(build(:service, type: 'Service', template: true)).to be_invalid
end end
end end
context 'with an existing instance service' do
before do
create(:service, :instance)
end
it 'validates only one service instance per type' do
expect(build(:service, :instance)).to be_invalid
end
end
end end
describe 'Scopes' do describe 'Scopes' do
......
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