diff --git a/config/feature_flags/development/optimize_scope_projects_with_feature_available.yml b/config/feature_flags/development/optimize_scope_projects_with_feature_available.yml new file mode 100644 index 0000000000000000000000000000000000000000..86811a6c534da410665178970e3f8e687791268f --- /dev/null +++ b/config/feature_flags/development/optimize_scope_projects_with_feature_available.yml @@ -0,0 +1,8 @@ +--- +name: optimize_scope_projects_with_feature_available +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119950/ +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410693 +milestone: '16.0' +type: development +group: group::tenant scale +default_enabled: false diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 4d339512fd43958738bbbbb4ef76ea7991fb91c8..98ad1873ce6bcc04cd2d163f785ae90df5949d0a 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -165,8 +165,20 @@ def lock_for_confirmation!(id) scope :verification_failed_wikis, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_wikis) } scope :for_plan_name, -> (name) { joins(namespace: { gitlab_subscription: :hosted_plan }).where(plans: { name: name }) } scope :with_feature_available, -> (name) do - projects_with_feature_available_in_plan = ::Project.for_group(::Group.with_feature_available_in_plan(name)) - public_projects_in_public_groups = ::Project.public_only.for_group(::Group.public_only) + if ::Feature.enabled?(:optimize_scope_projects_with_feature_available) + groups_of_these_projects = ::Group.id_in(select(:namespace_id)) + root_groups_of_these_projects = groups_of_these_projects.roots + + paid_groups = root_groups_of_these_projects.with_feature_available_in_plan(name) + # subgroups of a paid group inherit paid features of the root group, + # and hence we must also include projects from such subgroups. + projects_with_feature_available_in_plan = for_group_and_its_subgroups(paid_groups) + public_projects_in_public_groups = public_only.for_group(groups_of_these_projects.public_only) + else + projects_with_feature_available_in_plan = ::Project.for_group(::Group.with_feature_available_in_plan(name)) + public_projects_in_public_groups = ::Project.public_only.for_group(::Group.public_only) + end + from_union([projects_with_feature_available_in_plan, public_projects_in_public_groups]) end scope :requiring_code_owner_approval, diff --git a/ee/spec/finders/ee/projects_finder_spec.rb b/ee/spec/finders/ee/projects_finder_spec.rb index 596858b89d2e2f0ff50f1519108b4ea7de79b4da..5a9e67907086b50fc286111677ec5525893efdc3 100644 --- a/ee/spec/finders/ee/projects_finder_spec.rb +++ b/ee/spec/finders/ee/projects_finder_spec.rb @@ -81,10 +81,46 @@ end end + context 'filter by feature available' do + let_it_be(:private_premium_project) { create_project(:premium_plan, :private) } + + before do + private_premium_project.add_owner(user) + end + + context 'when feature_available filter is used' do + # `product_analytics` is a feature available in Ultimate tier only + let_it_be(:params) { { feature_available: 'product_analytics' } } + + it do + is_expected.to contain_exactly( + ultimate_project, + ultimate_project2, + premium_project, + no_plan_project + ) + end + end + + context 'when feature_available filter is not used' do + let_it_be(:params) { {} } + + it do + is_expected.to contain_exactly( + ultimate_project, + ultimate_project2, + premium_project, + no_plan_project, + private_premium_project + ) + end + end + end + private - def create_project(plan) - create(:project, :public, namespace: create(:namespace_with_plan, plan: plan)) + def create_project(plan, visibility = :public) + create(:project, visibility, namespace: create(:group_with_plan, plan: plan)) end end end diff --git a/ee/spec/models/ee/project_spec.rb b/ee/spec/models/ee/project_spec.rb index 8a101bf8a4142a05858097923459e05d3e18f8aa..dc449853a1644d0755a7d76c8baadc196e8d1bf1 100644 --- a/ee/spec/models/ee/project_spec.rb +++ b/ee/spec/models/ee/project_spec.rb @@ -558,18 +558,50 @@ end describe '.with_feature_available', :saas do + let_it_be(:user) { create(:user) } + + let_it_be(:ultimate_group) { create(:group_with_plan, :private, plan: :ultimate_plan) } + let_it_be(:ultimate_subgroup) { create(:group, :private, parent: ultimate_group) } + let_it_be(:another_ultimate_group) { create(:group_with_plan, :private, plan: :ultimate_plan) } + let_it_be(:another_ultimate_subgroup) { create(:group, :private, parent: another_ultimate_group) } + let_it_be(:premium_group) { create(:group_with_plan, :private, plan: :premium_plan) } + let_it_be(:premium_subgroup) { create(:group, :private, parent: premium_group) } + let_it_be(:no_plan_group) { create(:group_with_plan, :public, plan: nil) } + let_it_be(:no_plan_subgroup) { create(:group, :public, parent: no_plan_group) } + let_it_be(:ultimate_project) { create(:project, :private, :archived, creator: user, namespace: ultimate_group) } + let_it_be(:premium_project) { create(:project, :private, :archived, creator: user, namespace: premium_group) } + let_it_be(:no_plan_public_project) { create(:project, :public, :archived, creator: user, namespace: no_plan_group) } + let_it_be(:ultimate_subgroup_project) { create(:project, :private, :archived, creator: user, namespace: ultimate_subgroup) } + let_it_be(:another_ultimate_subgroup_project) { create(:project, :private, :archived, creator: user, namespace: another_ultimate_subgroup) } + let_it_be(:premium_subgroup_project) { create(:project, :private, :archived, creator: user, namespace: premium_subgroup) } + let_it_be(:no_plan_private_project) { create(:project, :private, :archived, creator: user, namespace: no_plan_group) } + let_it_be(:no_plan_subgroup_private_project) { create(:project, :private, :archived, creator: user, namespace: no_plan_subgroup) } + + subject(:result) { described_class.with_feature_available(:adjourned_deletion_for_projects_and_groups) } + it 'lists projects with the feature available' do - user = create(:user) - ultimate_group = create(:group_with_plan, plan: :ultimate_plan) - premium_group = create(:group_with_plan, plan: :premium_plan) - no_plan_group = create(:group_with_plan, plan: nil) - ultimate_project = create(:project, :archived, creator: user, namespace: ultimate_group) - premium_project = create(:project, :archived, creator: user, namespace: premium_group) - no_plan_project = create(:project, :archived, creator: user, namespace: no_plan_group) - no_plan_public_project = create(:project, :archived, creator: user, visibility: ::Gitlab::VisibilityLevel::PUBLIC, namespace: no_plan_group) - - expect(described_class.with_feature_available(:adjourned_deletion_for_projects_and_groups)).to contain_exactly(premium_project, ultimate_project, no_plan_public_project) - expect(described_class.with_feature_available(:adjourned_deletion_for_projects_and_groups)).not_to include(no_plan_project) + is_expected.to contain_exactly( + premium_project, + premium_subgroup_project, + ultimate_project, + ultimate_subgroup_project, + another_ultimate_subgroup_project, + no_plan_public_project + ) + end + + context 'when the feature flag `optimize_scope_projects_with_feature_available` is turned off' do + before do + stub_feature_flags(optimize_scope_projects_with_feature_available: false) + end + + it 'does not include projects from paid subgroups' do + is_expected.to contain_exactly( + premium_project, + ultimate_project, + no_plan_public_project + ) + end end end