Commit 88d7f03b authored by Arturo Herrero's avatar Arturo Herrero

Propagate integration to group descendants

After propagate group-level integrations
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44023, we missed
one scenario where the integration could inherit from another
integration from the ancestors, not the closest one, this is something
that we also fixed finding the closest integration
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45022.

Example: Having a group, a subgroup, and a project belonging to the
subgroup. If we update the group, it propagates the settings to the
subgroup and project (both having inherit_from_id = group.id (pointing
to the group) but if after that, we update the settings of the subgroup,
it doesn't propagate them to the project. This is because the project's
inherit_from_id is still pointing to the group id from the first
propagation.

This always updates the integrations of any descendants for a group
integration.
parent a71ec1a7
...@@ -274,6 +274,17 @@ class Service < ApplicationRecord ...@@ -274,6 +274,17 @@ class Service < ApplicationRecord
end end
end end
def self.inherited_descendant_integrations_for(integration)
inherit_from_ids =
where(type: integration.type, group: integration.group.self_and_ancestors)
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
])
end
def activated? def activated?
active active
end end
......
...@@ -5,12 +5,12 @@ module Admin ...@@ -5,12 +5,12 @@ module Admin
include PropagateService include PropagateService
def propagate def propagate
update_inherited_integrations
if integration.instance? if integration.instance?
update_inherited_integrations
create_integration_for_groups_without_integration if Feature.enabled?(:group_level_integrations) create_integration_for_groups_without_integration if Feature.enabled?(:group_level_integrations)
create_integration_for_projects_without_integration create_integration_for_projects_without_integration
else else
update_inherited_descendant_integrations
create_integration_for_groups_without_integration_belonging_to_group create_integration_for_groups_without_integration_belonging_to_group
create_integration_for_projects_without_integration_belonging_to_group create_integration_for_projects_without_integration_belonging_to_group
end end
...@@ -25,6 +25,13 @@ module Admin ...@@ -25,6 +25,13 @@ module Admin
PropagateIntegrationInheritWorker.perform_async(integration.id, min_id, max_id) PropagateIntegrationInheritWorker.perform_async(integration.id, min_id, max_id)
end end
end end
def update_inherited_descendant_integrations
Service.inherited_descendant_integrations_for(integration).in_batches(of: BATCH_SIZE) do |services|
min_id, max_id = services.pick("MIN(services.id), MAX(services.id)")
PropagateIntegrationInheritDescendantWorker.perform_async(integration.id, min_id, max_id)
end
end
# rubocop: enable Cop/InBatches # rubocop: enable Cop/InBatches
def create_integration_for_groups_without_integration def create_integration_for_groups_without_integration
......
...@@ -47,7 +47,7 @@ class BulkCreateIntegrationService ...@@ -47,7 +47,7 @@ class BulkCreateIntegrationService
if integration.template? if integration.template?
integration.to_service_hash integration.to_service_hash
else else
integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
end end
end end
......
...@@ -23,7 +23,7 @@ class BulkUpdateIntegrationService ...@@ -23,7 +23,7 @@ class BulkUpdateIntegrationService
attr_reader :integration, :batch attr_reader :integration, :batch
def service_hash def service_hash
integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
end end
def data_fields_hash def data_fields_hash
......
...@@ -1839,6 +1839,14 @@ ...@@ -1839,6 +1839,14 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: propagate_integration_inherit_descendant
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: propagate_integration_project - :name: propagate_integration_project
:feature_category: :integrations :feature_category: :integrations
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
class PropagateIntegrationInheritDescendantWorker
include ApplicationWorker
feature_category :integrations
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(integration_id, min_id, max_id)
integration = Service.find_by_id(integration_id)
return unless integration
services = Service.inherited_descendant_integrations_for(integration).where(id: min_id..max_id)
BulkUpdateIntegrationService.new(integration, services).execute
end
# rubocop: enable CodeReuse/ActiveRecord
end
...@@ -252,6 +252,8 @@ ...@@ -252,6 +252,8 @@
- 1 - 1
- - propagate_integration_inherit - - propagate_integration_inherit
- 1 - 1
- - propagate_integration_inherit_descendant
- 1
- - propagate_integration_project - - propagate_integration_project
- 1 - 1
- - propagate_service_template - - propagate_service_template
......
...@@ -599,6 +599,23 @@ RSpec.describe Service do ...@@ -599,6 +599,23 @@ RSpec.describe Service do
end end
end end
describe '.inherited_descendant_integrations_for' do
let(:subgroup1) { create(:group, parent: group) }
let(:subgroup2) { create(:group, parent: group) }
let(:project1) { create(:project, group: subgroup1) }
let(:project2) { create(:project, group: subgroup2) }
let!(:group_integration) { create(:prometheus_service, group: group, project: nil) }
let!(:subgroup_integration1) { create(:prometheus_service, group: subgroup1, project: nil, inherit_from_id: group_integration.id) }
let!(:subgroup_integration2) { create(:prometheus_service, group: subgroup2, project: nil) }
let!(:project_integration1) { create(:prometheus_service, group: nil, project: project1, inherit_from_id: group_integration.id) }
let!(:project_integration2) { create(:prometheus_service, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) }
it 'returns the groups and projects inheriting from integration ancestors' do
expect(described_class.inherited_descendant_integrations_for(group_integration)).to eq([subgroup_integration1, project_integration1])
expect(described_class.inherited_descendant_integrations_for(subgroup_integration2)).to eq([project_integration2])
end
end
describe "{property}_changed?" do describe "{property}_changed?" do
let(:service) do let(:service) do
BambooService.create( BambooService.create(
......
...@@ -67,7 +67,7 @@ RSpec.describe Admin::PropagateIntegrationService do ...@@ -67,7 +67,7 @@ RSpec.describe Admin::PropagateIntegrationService do
end end
end end
context 'with a group without integration' do context 'with a subgroup without integration' do
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
it 'calls to PropagateIntegrationGroupWorker' do it 'calls to PropagateIntegrationGroupWorker' do
...@@ -77,6 +77,18 @@ RSpec.describe Admin::PropagateIntegrationService do ...@@ -77,6 +77,18 @@ RSpec.describe Admin::PropagateIntegrationService do
described_class.propagate(group_integration) described_class.propagate(group_integration)
end end
end end
context 'with a subgroup with integration' do
let(:subgroup) { create(:group, parent: group) }
let(:subgroup_integration) { create(:jira_service, group: subgroup, project: nil, inherit_from_id: group_integration.id) }
it 'calls to PropagateIntegrationInheritDescendantWorker' do
expect(PropagateIntegrationInheritDescendantWorker).to receive(:perform_async)
.with(group_integration.id, subgroup_integration.id, subgroup_integration.id)
described_class.propagate(group_integration)
end
end
end end
end end
end end
...@@ -37,7 +37,7 @@ RSpec.describe BulkCreateIntegrationService do ...@@ -37,7 +37,7 @@ RSpec.describe BulkCreateIntegrationService do
it 'updates inherit_from_id attributes' do it 'updates inherit_from_id attributes' do
described_class.new(integration, batch, association).execute described_class.new(integration, batch, association).execute
expect(created_integration.reload.inherit_from_id).to eq(integration.id) expect(created_integration.reload.inherit_from_id).to eq(inherit_from_id)
end end
end end
...@@ -79,6 +79,7 @@ RSpec.describe BulkCreateIntegrationService do ...@@ -79,6 +79,7 @@ RSpec.describe BulkCreateIntegrationService do
context 'with an instance-level integration' do context 'with an instance-level integration' do
let(:integration) { instance_integration } let(:integration) { instance_integration }
let(:inherit_from_id) { integration.id }
context 'with a project association' do context 'with a project association' do
let!(:project) { create(:project) } let!(:project) { create(:project) }
...@@ -107,6 +108,19 @@ RSpec.describe BulkCreateIntegrationService do ...@@ -107,6 +108,19 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'creates integration from batch ids' it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id' it_behaves_like 'updates inherit_from_id'
context 'with a subgroup association' do
let!(:group_integration) { create(:jira_service, group: group, project: nil, inherit_from_id: instance_integration.id) }
let!(:subgroup) { create(:group, parent: group) }
let(:integration) { group_integration }
let(:created_integration) { Service.find_by(group: subgroup) }
let(:batch) { Group.all }
let(:association) { 'group' }
let(:inherit_from_id) { instance_integration.id }
it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id'
end
end end
end end
...@@ -118,6 +132,7 @@ RSpec.describe BulkCreateIntegrationService do ...@@ -118,6 +132,7 @@ RSpec.describe BulkCreateIntegrationService do
let(:created_integration) { project.jira_service } let(:created_integration) { project.jira_service }
let(:batch) { Project.all } let(:batch) { Project.all }
let(:association) { 'project' } let(:association) { 'project' }
let(:inherit_from_id) { integration.id }
it_behaves_like 'creates integration from batch ids' it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates project callbacks' it_behaves_like 'updates project callbacks'
......
...@@ -5,14 +5,28 @@ require 'spec_helper' ...@@ -5,14 +5,28 @@ require 'spec_helper'
RSpec.describe BulkUpdateIntegrationService do RSpec.describe BulkUpdateIntegrationService do
include JiraServiceHelper include JiraServiceHelper
before do before_all do
stub_jira_service_test stub_jira_service_test
end end
let(:excluded_attributes) { %w[id project_id group_id inherit_from_id instance template created_at updated_at] } let(:excluded_attributes) { %w[id project_id group_id inherit_from_id instance template created_at updated_at] }
let!(:instance_integration) do
let_it_be(:group) { create(:group) }
let_it_be(:group_integration) do
JiraService.create!(
group: group,
active: true,
push_events: true,
url: 'http://update-jira.instance.com',
username: 'user',
password: 'secret'
)
end
let_it_be(:subgroup_integration) do
JiraService.create!( JiraService.create!(
instance: true, inherit_from_id: group_integration.id,
group: create(:group, parent: group),
active: true, active: true,
push_events: true, push_events: true,
url: 'http://update-jira.instance.com', url: 'http://update-jira.instance.com',
...@@ -21,10 +35,9 @@ RSpec.describe BulkUpdateIntegrationService do ...@@ -21,10 +35,9 @@ RSpec.describe BulkUpdateIntegrationService do
) )
end end
let!(:integration) do let_it_be(:integration) do
JiraService.create!( JiraService.create!(
project: create(:project), project: create(:project),
inherit_from_id: instance_integration.id,
instance: false, instance: false,
active: true, active: true,
push_events: false, push_events: false,
...@@ -36,21 +49,21 @@ RSpec.describe BulkUpdateIntegrationService do ...@@ -36,21 +49,21 @@ RSpec.describe BulkUpdateIntegrationService do
context 'with inherited integration' do context 'with inherited integration' do
it 'updates the integration' do it 'updates the integration' do
described_class.new(instance_integration, Service.inherit_from_id(instance_integration.id)).execute described_class.new(subgroup_integration, Service.where.not(project: nil)).execute
expect(integration.reload.inherit_from_id).to eq(instance_integration.id) expect(integration.reload.inherit_from_id).to eq(group_integration.id)
expect(integration.attributes.except(*excluded_attributes)) expect(integration.attributes.except(*excluded_attributes))
.to eq(instance_integration.attributes.except(*excluded_attributes)) .to eq(subgroup_integration.attributes.except(*excluded_attributes))
end end
context 'with integration with data fields' do context 'with integration with data fields' do
let(:excluded_attributes) { %w[id service_id created_at updated_at] } let(:excluded_attributes) { %w[id service_id created_at updated_at] }
it 'updates the data fields from the integration' do it 'updates the data fields from the integration' do
described_class.new(instance_integration, Service.inherit_from_id(instance_integration.id)).execute described_class.new(subgroup_integration, Service.where.not(project: nil)).execute
expect(integration.reload.data_fields.attributes.except(*excluded_attributes)) expect(integration.reload.data_fields.attributes.except(*excluded_attributes))
.to eq(instance_integration.data_fields.attributes.except(*excluded_attributes)) .to eq(subgroup_integration.data_fields.attributes.except(*excluded_attributes))
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PropagateIntegrationInheritDescendantWorker do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_integration) { create(:redmine_service, group: group, project: nil) }
let_it_be(:subgroup_integration) { create(:redmine_service, group: subgroup, project: nil, inherit_from_id: group_integration.id) }
it_behaves_like 'an idempotent worker' do
let(:job_args) { [group_integration.id, subgroup_integration.id, subgroup_integration.id] }
it 'calls to BulkUpdateIntegrationService' do
expect(BulkUpdateIntegrationService).to receive(:new)
.with(group_integration, match_array(subgroup_integration)).twice
.and_return(double(execute: nil))
subject
end
end
context 'with an invalid integration id' do
it 'returns without failure' do
expect(BulkUpdateIntegrationService).not_to receive(:new)
subject.perform(0, subgroup_integration.id, subgroup_integration.id)
end
end
end
...@@ -12,7 +12,7 @@ RSpec.describe PropagateIntegrationInheritWorker do ...@@ -12,7 +12,7 @@ RSpec.describe PropagateIntegrationInheritWorker do
it_behaves_like 'an idempotent worker' do it_behaves_like 'an idempotent worker' do
let(:job_args) { [integration.id, integration1.id, integration3.id] } let(:job_args) { [integration.id, integration1.id, integration3.id] }
it 'calls to BulkCreateIntegrationService' do it 'calls to BulkUpdateIntegrationService' do
expect(BulkUpdateIntegrationService).to receive(:new) expect(BulkUpdateIntegrationService).to receive(:new)
.with(integration, match_array(integration1)).twice .with(integration, match_array(integration1)).twice
.and_return(double(execute: nil)) .and_return(double(execute: nil))
......
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