Enforce ci_lint rate limit on lint endpoints behind feature flag

What does this MR do and why?

Enables CI Lint rate limits on various lint entry points, gated behind a feature flag so we can roll it out safely. For the time being adds logging when rate limit is throttled.

  • Adds Gitlab::Ci::Lint::RateLimitError and raises it from both Gitlab::Ci::Lint#legacy_validate and #validate when a request is throttled. This is the single chokepoint all lint paths flow through.
  • Adds #ci_lint_rate_limited? which increments the :ci_lint counter (scoped to the current user), logs every over-limit request, but only returns true (i.e. actually rejects) when the ci_enforce_ci_lint_rate_limit flag is enabled.
  • Translates the error into the right response at each entry point:
    • REST API (lib/api/lint.rb) and the controller → 429 Too Many Requests with a Retry-After header and a JSON error message.
    • GraphQL mutation and resolver → raise_resource_not_available_error!.
  • Excludes lint requests from the pipeline-creation rate limiter (Chain::Limit::RateLimit now returns early when command.linting?)
  • This MR doesnt cover blob viewer updates, which have been removed here

References

Relates to https://gitlab.com/gitlab-org/gitlab/-/work_items/599486

How to set up and validate locally

  1. Adjust the CI Lint rate limit (Admin → Settings UI (Pipeline limits section)). In rails console check Gitlab::ApplicationRateLimiter.threshold(:ci_lint) and Gitlab::ApplicationRateLimiter.interval(:ci_lint)
  2. Either using a test or manually-
    1. Validate log-only mode (flag is disabled: Feature.disabled?(:ci_enforce_ci_lint_rate_limit) )
    2. Hit the REST endpoint repeatedly past the limit:
for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{http_code}\n" 
  --header "PRIVATE-TOKEN: <token>" 
  --header "Content-Type: application/json" 
  --data '{"content": "build:\n script: echo hello"}' 
  "http://127.0.0.1:3000/api/v4/projects/<project_id>/ci/lint"
done
  1. Verify every request returns 200, and we can see logs for rate limit
  2. Enable the flag and reset the counter: Feature.enable(:ci_enforce_ci_lint_rate_limit) gdk redis-cli flushdb (or clear the rate_limiting redis to reset counters)
  3. Re-run the curl loop from step 2. Expected: the first requests return 200, then once the limit is exceeded you get:
  • HTTP 429 Too Many Requests with a Retry-After header
  • JSON body {"error": "This endpoint has been requested too many times. Try again later."}
  1. Validate the GraphQL path. With the flag enabled and the limit exceeded, run the ciLint mutation (or ciConfig query):
mutation {
  ciLint(input: { projectPath: "<group/project>", content: "build:\n script: echo" }) {
    errors
    config { mergedYaml }
  }
}
  1. Verify once throttled, the response contains a "resource not available" error instead of a lint result.
  2. Confirm pipeline-creation limit is unaffected.

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 #599486

Edited by Tarun Raja

Merge request reports

Loading