Add opt-in offline egress guard for RSpec

What does this MR do and why?

An instance on an offline cloud licence must never reach a GitLab cloud host. Today a spec can pass while product code on that licence resolves cloud.gitlab.com, so a new call path can merge with no failing spec.

This adds a guard to ee/spec/support that turns each such resolution into a named failure. It is opt-in: with OFFLINE_EGRESS_GUARD unset, nothing is installed.

  • Set OFFLINE_EGRESS_GUARD=1 and the guard watches four resolver methods and every WebMock request for a cloud host:
    • Gitlab::AiGateway.cloud_connector_url and .cloud_connector_auth_url
    • Gitlab::DuoWorkflow::Client.cloud_connected_url
    • Gitlab::TelemetryEndpoint.version_check_url
  • It raises OfflineEgressGuard::OfflineEgressError for a cloud host only when License.current is an offline cloud licence, so a spec that builds its own offline licence is covered with no setup. For the version host it raises on any licence while the version check is off, and only where version_check_url resolves it: Service Ping posts to the same host and that setting does not govern it.
  • The error names the URL, the resolver, the first app or lib line that called it, and the example.
  • An example that expects the call tags itself :allow_offline_egress, a key added to spec/support/known_rspec_metadata_keys.yml.
  • The error inherits from Exception, so a rescue StandardError in product code cannot hide it.
  • It follows the env-gated prepend in spec/support/permissions_check.rb:6 (GITLAB_DEBUG_POLICIES), and reads its switch the same way, through Gitlab::Utils.to_boolean.
  • The WebMock check uses WebMock.globally_stub_request, which nothing else in spec/ uses: it runs before local stubs, so a spec's own stub cannot hide a cloud request.
  • The hosts are cloud.gitlab.com, customers.gitlab.com, their staging twins, *.runway.gitlab.net and the host of gitlab.cloud_connector.base_url.
flowchart TD
  S["OFFLINE_EGRESS_GUARD=1<br/>set at suite start?"] -->|"no"| U["nothing installed<br/>suite unchanged"]
  S -->|"yes"| I["install: prepend resolver checks<br/>and a WebMock stub before local stubs"]
  I --> C{"example resolves a cloud or version host,<br/>or requests a cloud host?"}
  C -->|"no"| P["passes through"]
  C -->|"yes"| L{"cloud host and an offline cloud licence,<br/>or version host and version check off?"}
  L -->|"no"| P
  L -->|"yes"| T{"example tagged<br/>:allow_offline_egress?"}
  T -->|"yes"| P
  T -->|"no"| X["OfflineEgressError<br/>names URL, resolver, caller, example"]

The guard sees URLs the four resolver methods return and requests made through WebMock. It does not see a request stubbed with an RSpec double, a gRPC channel opened from a URL it did not resolve, Workhorse, or the Python AI gateway.

A CI job that runs specs on an offline licence is a separate, later step and is not in this MR.

This is an offer. Reviewers can take it, reshape it or close it, and the choices above (the env switch, the tag, the host list) are easy to change.

References

Review areas:

  • group::ai model services (@mkomor1): the host list and the resolver wrappers, which have to track AI gateway and Cloud Connector changes.
  • Developer Experience (group::development health, @treagitlab): ee/spec/support, the env switch and the metadata key. .gitlab/CODEOWNERS lists @gl-dx/maintainers for /spec/ and /ee/spec/.

Screenshots or screen recordings

Not applicable: this MR changes specs only.

How to set up and validate locally

  1. Run the guard's own spec, which covers the check, the allowlist tag, the resolver wrappers and the WebMock hook:
    bundle exec rspec ee/spec/support_specs/offline_egress_guard_spec.rb
  2. Run the specs for the resolvers with the guard on. On the default licence it adds no failures:
    OFFLINE_EGRESS_GUARD=1 bundle exec rspec ee/spec/lib/gitlab/ai_gateway_spec.rb ee/spec/lib/gitlab/duo_workflow ee/spec/lib/gitlab/ai spec/lib/gitlab/telemetry_endpoint_spec.rb ee/spec/support_specs
    That is 583 examples and 0 failures, the same as without the variable.
  3. To see it fire, stub License.current with build_stubbed(:license, data: build(:gitlab_license, :offline).export) in an example and call Gitlab::AiGateway.cloud_connector_url. With the variable set it raises OfflineEgressError. Without it, nothing raises.

I ran that last check through all four resolver methods, a direct Net::HTTP request to cloud.gitlab.com and a Gitlab::HTTP request to customers.gitlab.com, in one throwaway spec of 10 examples: 10 pass with the variable set, and the 6 that expect a raise fail without it.

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.

An AI coding assistant wrote this change and ran RuboCop, the guard's spec, the 583-example run and the throwaway spec above on a development GitLab instance on a test VM.

Edited by Andrew Dunn

Merge request reports

Loading
Loading