Skip to content

Introduce limit to number of registered runners [RUN ALL RSPEC] [RUN AS-IF-FOSS]

What does this MR do?

NOTE: This MR is gated behind a :ci_runner_limits FF. Rollout issue: #329438 (closed)

This MR adds :ci_registered_(instance|group|project)_runners plan limits to ::Ci::Runner so that new runners cannot be registered when the limit is exceeded to prevent abuse. The limit is enforced by the Limitable concern. Since the different runner scopes are all embodied by the single Ci::Runner class, the Limitable implementation needed to be extended in !60302 (closed) (on which this MR is based).

How to test this MR

  1. Check out this branch and lower the limits in db/migrate/20210423155059_add_runner_registration_to_plan_limits.rb so that you can quickly hit the limits. For ci_registered_instance_runners this will depend on how many instance runners you already have registered in the instance.

  2. Run the migrations:

    rails db:migrate RAILS_ENV=development
  3. Create a test project called test-runner-limit at http://localhost:3000/root/

  4. Testing project limits:

    1. Navigate to http://localhost:3000/root/test-runner-limit/-/settings/ci_cd#js-runners-settings and start registering runners using the designated runner registration token until you reach the limit defined for :ci_registered_project_runners in step 1:

      Registration command
      # Write to /tmp/config.gdk.toml so we can just unregister all runners from that file at the end 
      gitlab-runner register -config /tmp/config.gdk.toml \
              --non-interactive \
              --executor "shell" \
              --url "http://localhost:3000/" \
              --description "project test runner" \
              --tag-list "shell,gdk" \
              --run-untagged="false" \
              --locked="false" \
              --access-level="not_protected" \
              --registration-token="..." # Project runner registration token
    2. Once the limit is reached you should start getting HTTP 400 errors from gitlab-runner register

    3. You can also verify that reassigning other runners to this project is disallowed if the limit has been reached

  5. Testing group limits:

    1. Navigate to e.g. http://localhost:3000/groups/gitlab-org/-/settings/ci_cd#js-runners-settings (this group should exist in the GDK installation) and start registering runners using the designated runner registration token until you reach the limit defined for :ci_registered_group_runners in step 1:

      Registration command
      # Write to /tmp/config.gdk.toml so we can just unregister all runners from that file at the end 
      gitlab-runner register -config /tmp/config.gdk.toml \
              --non-interactive \
              --executor "shell" \
              --url "http://localhost:3000/" \
              --description "group test runner" \
              --tag-list "shell,gdk" \
              --run-untagged="false" \
              --locked="false" \
              --access-level="not_protected" \
              --registration-token="..." # Group runner registration token
    2. Once the limit is reached you should start getting HTTP 400 errors from gitlab-runner register

    3. You can also verify that creating other runners in other groups is disallowed if the limit has been reached

  6. Testing instance limits:

    1. Navigate to http://localhost:3000/admin/runners and start registering runners using the designated runner registration token until you reach the limit defined for :ci_registered_instance_runners in step 1:

      Registration command
      # Write to /tmp/config.gdk.toml so we can just unregister all runners from that file at the end 
      gitlab-runner register -config /tmp/config.gdk.toml \
              --non-interactive \
              --executor "shell" \
              --url "http://localhost:3000/" \
              --description "instance test runner" \
              --tag-list "shell,gdk" \
              --run-untagged="false" \
              --locked="false" \
              --access-level="not_protected" \
              --registration-token="..." # Instance runner registration token
    2. Once the limit is reached you should start getting HTTP 400 errors from gitlab-runner register

  7. Clean-up:

    # Unregister all runners in temporary config file
    gitlab-runner --config /tmp/config.gdk.toml unregister --all-runners && rm -f /tmp/config.gdk.toml
    # Rollback database migrations
    rails db:migrate:down VERSION=20210423164702 && rails db:migrate:down VERSION=20210423155059

Database migration logs

rails db:migrate RAILS_ENV=development
== 20210423155059 AddRunnerRegistrationToPlanLimits: migrating ================
-- add_column(:plan_limits, :ci_registered_instance_runners, :integer, {:default=>10000, :null=>false})
   -> 0.0017s
-- add_column(:plan_limits, :ci_registered_group_runners, :integer, {:default=>2000, :null=>false})
   -> 0.0008s
-- add_column(:plan_limits, :ci_registered_project_runners, :integer, {:default=>100, :null=>false})
   -> 0.0010s
== 20210423155059 AddRunnerRegistrationToPlanLimits: migrated (0.0100s) =======

== 20210423164702 InsertRunnerRegistrationPlanLimits: migrating ===============
-- quote_column_name("ci_registered_instance_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(10000)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_instance_runners\")\nSELECT id, '10000' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_instance_runners\" = EXCLUDED.\"ci_registered_instance_runners\";\n")
   -> 0.0017s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(2000)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '2000' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0008s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("free")
   -> 0.0000s
-- quote(20)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '20' FROM plans WHERE name = 'free' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0006s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("bronze")
   -> 0.0000s
-- quote(1000)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '1000' FROM plans WHERE name = 'bronze' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0009s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("silver")
   -> 0.0000s
-- quote(2000)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '2000' FROM plans WHERE name = 'silver' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0009s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("gold")
   -> 0.0000s
-- quote(2000)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '2000' FROM plans WHERE name = 'gold' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0009s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(100)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '100' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0012s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("free")
   -> 0.0000s
-- quote(5)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '5' FROM plans WHERE name = 'free' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0008s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("bronze")
   -> 0.0000s
-- quote(50)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '50' FROM plans WHERE name = 'bronze' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0008s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("silver")
   -> 0.0000s
-- quote(100)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '100' FROM plans WHERE name = 'silver' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0010s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("gold")
   -> 0.0000s
-- quote(100)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '100' FROM plans WHERE name = 'gold' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0008s
== 20210423164702 InsertRunnerRegistrationPlanLimits: migrated (0.0374s) ======

rails db:migrate RAILS_ENV=development  8.23s user 4.05s system 85% cpu 14.288 total
rails db:rollback STEP=2 RAILS_ENV=development
== 20210423164702 InsertRunnerRegistrationPlanLimits: reverting ===============
-- quote_column_name("ci_registered_instance_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_instance_runners\")\nSELECT id, '0' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_instance_runners\" = EXCLUDED.\"ci_registered_instance_runners\";\n")
   -> 0.0034s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '0' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0015s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("free")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '0' FROM plans WHERE name = 'free' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0012s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("bronze")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '0' FROM plans WHERE name = 'bronze' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0014s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("silver")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '0' FROM plans WHERE name = 'silver' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0011s
-- quote_column_name("ci_registered_group_runners")
   -> 0.0000s
-- quote("gold")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_group_runners\")\nSELECT id, '0' FROM plans WHERE name = 'gold' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_group_runners\" = EXCLUDED.\"ci_registered_group_runners\";\n")
   -> 0.0013s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("default")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '0' FROM plans WHERE name = 'default' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0013s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("free")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '0' FROM plans WHERE name = 'free' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0011s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("bronze")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '0' FROM plans WHERE name = 'bronze' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0016s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("silver")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '0' FROM plans WHERE name = 'silver' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0013s
-- quote_column_name("ci_registered_project_runners")
   -> 0.0000s
-- quote("gold")
   -> 0.0000s
-- quote(0)
   -> 0.0000s
-- execute("INSERT INTO plan_limits (plan_id, \"ci_registered_project_runners\")\nSELECT id, '0' FROM plans WHERE name = 'gold' LIMIT 1\nON CONFLICT (plan_id) DO UPDATE SET \"ci_registered_project_runners\" = EXCLUDED.\"ci_registered_project_runners\";\n")
   -> 0.0013s
== 20210423164702 InsertRunnerRegistrationPlanLimits: reverted (0.0182s) ======

== 20210423155059 AddRunnerRegistrationToPlanLimits: reverting ================
-- column_exists?(:plan_limits, :ci_registered_instance_runners)
   -> 0.0041s
-- remove_column(:plan_limits, :ci_registered_instance_runners)
   -> 0.0019s
-- column_exists?(:plan_limits, :ci_registered_group_runners)
   -> 0.0049s
-- remove_column(:plan_limits, :ci_registered_group_runners)
   -> 0.0017s
-- column_exists?(:plan_limits, :ci_registered_project_runners)
   -> 0.0047s
-- remove_column(:plan_limits, :ci_registered_project_runners)
   -> 0.0016s
== 20210423155059 AddRunnerRegistrationToPlanLimits: reverted (0.0304s) =======

rails db:rollback STEP=2 RAILS_ENV=development  8.15s user 3.94s system 88% cpu 13.685 total

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Security

If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:

  • Label as security and @ mention @gitlab-com/gl-security/appsec
  • The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • Security reports checked/validated by a reviewer from the AppSec team

Part of #321368

Edited by Pedro Pombeiro

Merge request reports