Commit b25634b1 authored by Luke Duncalfe's avatar Luke Duncalfe

Expose projects with integration overrides

This exposes a paginated JSON list of projects that override an
instance-level integration.

https://gitlab.com/gitlab-org/gitlab/-/issues/218252
parent 440e4556
......@@ -8,6 +8,17 @@ class Admin::IntegrationsController < Admin::ApplicationController
feature_category :integrations
def overrides
respond_to do |format|
format.json do
collection = Project.with_active_integration(integration.class).merge(::Integration.not_inherited)
render json: collection.page(params[:page]).per(20).to_json
end
# TODO frontend will add format.html
end
end
private
def find_or_initialize_non_project_specific_integration(name)
......
......@@ -4,18 +4,6 @@ module HasIntegrations
extend ActiveSupport::Concern
class_methods do
def with_custom_integration_for(integration, page = nil, per = nil)
custom_integration_project_ids = Integration
.select(:project_id)
.where(type: integration.type)
.where(inherit_from_id: nil)
.where.not(project_id: nil)
.page(page)
.per(per)
Project.where(id: custom_integration_project_ids)
end
def without_integration(integration)
integrations = Integration
.select('1')
......
......@@ -78,6 +78,7 @@ class Integration < ApplicationRecord
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :inherit, -> { where.not(inherit_from_id: nil) }
scope :not_inherited, -> { where(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
......
......@@ -542,7 +542,6 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_active_jira_integrations, -> { joins(:integrations).merge(::Integrations::Jira.active) }
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
......@@ -550,7 +549,9 @@ class Project < ApplicationRecord
scope :with_namespace, -> { includes(:namespace) }
scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
scope :with_integration, ->(integration) { joins(integration).eager_load(integration) }
scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
......
......@@ -16,9 +16,11 @@ module Clusters
cluster = Clusters::Cluster.find_by_id(cluster_id)
raise cluster_missing_error(integration_name) unless cluster
integration = ::Project.integration_association_name(integration_name).to_sym
cluster.all_projects.with_integration(integration).find_each do |project|
project.public_send(integration).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
integration_class = Integration.integration_name_to_model(integration_name)
integration_association_name = ::Project.integration_association_name(integration_name).to_sym
cluster.all_projects.with_integration(integration_class).include_integration(integration_association_name).find_each do |project|
project.public_send(integration_association_name).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
end
end
......
......@@ -127,6 +127,7 @@ namespace :admin do
resource :application_settings, only: :update do
resources :integrations, only: [:edit, :update] do
member do
get :overrides
put :test
post :reset
end
......
......@@ -652,9 +652,9 @@ module Gitlab
todos: distinct_count(::Todo.where(time_period), :author_id),
service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period),
service_desk_issues: count(::Issue.service_desk.where(time_period)),
projects_jira_active: distinct_count(::Project.with_active_jira_integrations.where(time_period), :creator_id),
projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_jira_integrations.with_jira_dvcs_cloud.where(time_period), :creator_id),
projects_jira_dvcs_server_active: distinct_count(::Project.with_active_jira_integrations.with_jira_dvcs_server.where(time_period), :creator_id)
projects_jira_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .where(time_period), :creator_id),
projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .with_jira_dvcs_cloud.where(time_period), :creator_id),
projects_jira_dvcs_server_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .with_jira_dvcs_server.where(time_period), :creator_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -97,4 +97,24 @@ RSpec.describe Admin::IntegrationsController do
.and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
describe '#overrides' do
let_it_be(:instance_integration) { create(:bugzilla_integration, :instance) }
let_it_be(:non_overridden_integration) { create(:bugzilla_integration, inherit_from_id: instance_integration.id) }
let_it_be(:overridden_integration) { create(:bugzilla_integration) }
let_it_be(:overridden_other_integration) { create(:confluence_integration) }
subject do
get :overrides, params: { id: instance_integration.class.to_param }, format: :json
end
include_context 'JSON response'
it 'returns projects with overrides', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to contain_exactly(a_hash_including('id' => overridden_integration.project_id))
end
end
end
......@@ -17,14 +17,6 @@ RSpec.describe HasIntegrations do
create(:integrations_slack, project: project_4, inherit_from_id: nil)
end
describe '.with_custom_integration_for' do
it 'returns projects with custom integrations' do
# We use pagination to verify that the group is excluded from the query
expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3)
expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3)
end
end
describe '.without_integration' do
it 'returns projects without integration' do
expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
......
......@@ -61,6 +61,24 @@ RSpec.describe Integration do
end
describe 'Scopes' do
describe '.inherit' do
it 'returns the correct integrations' do
instance_integration = create(:integration, :instance)
inheriting_integration = create(:integration, inherit_from_id: instance_integration.id)
expect(described_class.inherit).to match_array([inheriting_integration])
end
end
describe '.not_inherited' do
it 'returns the correct integrations' do
instance_integration = create(:integration, :instance)
create(:integration, inherit_from_id: instance_integration.id)
expect(described_class.not_inherited).to match_array([instance_integration])
end
end
describe '.by_type' do
let!(:service1) { create(:jira_integration) }
let!(:service2) { create(:jira_integration) }
......
......@@ -1487,33 +1487,21 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '.with_active_jira_integrations' do
it 'returns the correct integrations' do
active_jira_integration = create(:jira_integration)
active_service = create(:service, active: true)
expect(described_class.with_active_jira_integrations).to include(active_jira_integration.project)
expect(described_class.with_active_jira_integrations).not_to include(active_service.project)
end
end
describe '.with_jira_dvcs_cloud' do
it 'returns the correct project' do
jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
jira_dvcs_server_project = create(:project, :jira_dvcs_server)
create(:project, :jira_dvcs_server)
expect(described_class.with_jira_dvcs_cloud).to include(jira_dvcs_cloud_project)
expect(described_class.with_jira_dvcs_cloud).not_to include(jira_dvcs_server_project)
expect(described_class.with_jira_dvcs_cloud).to contain_exactly(jira_dvcs_cloud_project)
end
end
describe '.with_jira_dvcs_server' do
it 'returns the correct project' do
jira_dvcs_server_project = create(:project, :jira_dvcs_server)
jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
create(:project, :jira_dvcs_cloud)
expect(described_class.with_jira_dvcs_server).to include(jira_dvcs_server_project)
expect(described_class.with_jira_dvcs_server).not_to include(jira_dvcs_cloud_project)
expect(described_class.with_jira_dvcs_server).to contain_exactly(jira_dvcs_server_project)
end
end
......@@ -1599,15 +1587,39 @@ RSpec.describe Project, factory_default: :keep do
end
describe '.with_integration' do
before do
create_list(:prometheus_project, 2)
it 'returns the correct projects' do
active_confluence_integration = create(:confluence_integration)
inactive_confluence_integration = create(:confluence_integration, active: false)
create(:bugzilla_integration)
expect(described_class.with_integration(::Integrations::Confluence)).to contain_exactly(
active_confluence_integration.project,
inactive_confluence_integration.project
)
end
end
describe '.with_active_integration' do
it 'returns the correct projects' do
active_confluence_integration = create(:confluence_integration)
create(:confluence_integration, active: false)
create(:bugzilla_integration, active: true)
expect(described_class.with_active_integration(::Integrations::Confluence)).to contain_exactly(
active_confluence_integration.project
)
end
end
let(:integration) { :prometheus_integration }
describe '.include_integration' do
it 'avoids n + 1', :aggregate_failures do
create(:prometheus_integration)
run_test = -> { described_class.include_integration(:prometheus_integration).map(&:prometheus_integration) }
control_count = ActiveRecord::QueryRecorder.new { run_test.call }
create(:prometheus_integration)
it 'avoids n + 1' do
expect { described_class.with_integration(integration).map(&integration) }
.not_to exceed_query_limit(1)
expect(run_test.call.count).to eq(2)
expect { run_test.call }.not_to exceed_query_limit(control_count)
end
end
......
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