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
-
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. Forci_registered_instance_runners
this will depend on how many instance runners you already have registered in the instance. -
Run the migrations:
rails db:migrate RAILS_ENV=development
-
Create a test project called
test-runner-limit
at http://localhost:3000/root/ -
Testing project limits:
-
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
-
Once the limit is reached you should start getting
HTTP 400
errors fromgitlab-runner register
-
You can also verify that reassigning other runners to this project is disallowed if the limit has been reached
-
-
Testing group limits:
-
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
-
Once the limit is reached you should start getting
HTTP 400
errors fromgitlab-runner register
-
You can also verify that creating other runners in other groups is disallowed if the limit has been reached
-
-
Testing instance limits:
-
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
-
Once the limit is reached you should start getting
HTTP 400
errors fromgitlab-runner register
-
-
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
-
📋 Does this MR need a changelog?- [] I have included a changelog entry.
-
I have not included a changelog entry because it is behind a feature flag.
-
Documentation (if required) -
Code review guidelines -
Merge request performance guidelines -
Style guides -
Database guides -
Separation of EE specific content
Availability and Testing
-
Review and add/update tests for this feature/bug. Consider all test levels. See the Test Planning Process. -
Tested in all supported browsers -
Informed Infrastructure department of a default or new setting change, if applicable per definition of done
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