Enforce upper limit for push mirrors per project

What does this MR do and why?

  1. Validation on create: Prevents new mirrors from being created when a project already has 10 enabled push mirrors
  2. Sync-time check: Existing mirrors that exceed the limit (created before this change) will be marked as failed during sync, with only the first 10 mirrors (by ID) allowed to sync

Based on GitLab.com data, there are existing projects with more than 10 mirrors:

  • 265 projects have >10 mirrors
  • 40 projects have >20 mirrors
  • 3 projects have >100 mirrors The sync-time check ensures these pre-existing mirrors are handled rather than continuing to overload the system.

Query optimization To avoid N+1 queries when syncing multiple mirrors, allowed_ids is computed once in Project#update_remote_mirrors and passed to each mirror's sync method. For single-mirror sync (via API/UI), the method falls back to a database query.

References

Screenshots or screen recordings

Before After

How to set up and validate locally

Test 1: Validation prevents creating mirrors beyond the limit

  1. Open Rails console
  2. Run the following:
project = create(:project, :repository)
# Create 10 enabled mirrors (at the limit)
10.times { |i| project.remote_mirrors.create!(url: "http://mirror#{i}.example.com", enabled: true) }
# Attempt to create an 11th mirror - should fail validation
mirror = project.remote_mirrors.new(url: "http://mirror10.example.com", enabled: true)
mirror.valid?  # => false
mirror.errors[:base]  # => ["Maximum number of remote mirrors (10) exceeded for this project."]

Test 2: Disabled mirrors don't count toward the limit

project = create(:project, :repository)
# Create 8 enabled + 5 disabled mirrors
8.times { |i| project.remote_mirrors.create!(url: "http://enabled#{i}.example.com", enabled: true) }
5.times { |i| project.remote_mirrors.create!(url: "http://disabled#{i}.example.com", enabled: false) }
# Can still create new enabled mirrors (only 8 enabled)
mirror = project.remote_mirrors.new(url: "http://new.example.com", enabled: true)
mirror.valid?  # => true

Test 3: Sync hard-fails mirrors beyond the limit

project = create(:project, :repository)
# Create 10 mirrors
10.times { |i| project.remote_mirrors.create!(url: "http://mirror#{i}.example.com", enabled: true) }
# Force-create an 11th mirror (simulating pre-existing data)
extra = project.remote_mirrors.new(url: "http://extra.example.com", enabled: true)
extra.save!(validate: false)
# Sync should hard-fail
extra.sync
extra.reload.update_status  # => "failed"
extra.last_error  # => "Push mirror limit (10) exceeded..."

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #580289

Edited by Nav

Merge request reports

Loading