Expose the instance plan in the metadata API
What does this MR do and why?
Adds a plan field to the instance metadata already served to every authenticated caller, in both its REST form (GET /api/v4/metadata, and its GET /api/v4/version alias) and its GraphQL form (the Metadata type the REST endpoint is implemented on top of).
Today GET /api/v4/license is the only endpoint that reports which plan an instance is licensed for, and it requires an administrator. Every client that adapts to what the instance supports therefore has to be configured by hand, or infer the plan by probing licensed endpoints, which each one does slightly differently. The plan is already inferable that way by any authenticated user, so what the current restriction protects is not the information, only the convenience of reading it.
The value is the plan of the current license (starter, premium, ultimate), free on an instance with no license and on CE where there is none to read, and null where subscriptions are held per namespace rather than per instance. That last case is GitLab.com: the instance license says nothing there about what any given caller may do, so reporting it would be worse than reporting nothing, because a caller would read it as their own entitlement. Callers there already have the per-namespace plan that ee/lib/ee/api/entities/namespace.rb exposes under can_admin_namespace || has_gitlab_subscription.
Nothing commercial is exposed. The licensee name and email, the seat count, the expiry date, the subscription identifier and the historical seat usage all stay behind the administrator check on /license, which this MR does not touch. One string is added, and it is the one every integration is already guessing.
The CE and EE split follows Gitlab::Tracking::StandardContext, which resolves a plan exactly this way, down to its 'free' # GitLab CE edition is always free base case. The EE override asks Gitlab::Saas.feature_available?(:gitlab_com_subscriptions) rather than Gitlab.com?, so it does not add a new entry to the Gitlab/AvoidGitlabInstanceChecks todo list.
Why this is safe to expose
- It sits behind authentication.
/api/v4/metadataalready hasbefore { authenticate! }and the GraphQL type already requires:read_instance_metadata. Anonymous callers learn nothing new. - No new permission is introduced. The field is served to exactly the callers who can read this endpoint today.
- It is the field next door to
enterprise, which already discloses EE versus CE to the same callers.
References
Closes #630305
That issue carries the measurements behind this change (a licensed EE 19.3.1 and GitLab.com, each asked with administrator and ordinary credentials), the prior proposals, and why this one is shaped differently from them.
Related: #247915 (expose the tier to all users on Self-Managed, open since 2020) and #219732 (read license information as a non-admin, open since 2020, linked to two customer tickets). Neither thread records a design objection. This MR deliberately does not do what #219732 asks, which is to open /api/v4/license itself; that endpoint carries contractual data a non-admin has no business reading, and that is likely why it never moved.
Screenshots or screen recordings
Not applicable, this is an API field with no UI surface.
How to set up and validate locally
On a licensed instance, as a user who is not an administrator:
curl --header "PRIVATE-TOKEN: <non_admin_token>" \
--url "https://gitlab.example.com/api/v4/metadata"{
"version": "19.5.0-ee",
"revision": "ceb07b24cb0",
"kas": { "enabled": true, "externalUrl": "grpc://gitlab.example.com:8150", "externalK8sProxyUrl": "https://gitlab.example.com:8150/k8s-proxy", "version": "19.5.0" },
"enterprise": true,
"plan": "ultimate"
}The same through GraphQL:
{ metadata { version enterprise plan } }The specs covering each case:
bundle exec rspec spec/models/app_config/instance_metadata_spec.rb
bundle exec rspec spec/requests/api/metadata_spec.rb
bundle exec rspec spec/graphql/types/app_config/instance_metadata_type_spec.rb
bundle exec rspec ee/spec/models/ee/app_config/instance_metadata_spec.rb
bundle exec rspec ee/spec/requests/api/metadata_spec.rbee/spec/requests/api/metadata_spec.rb is the one that covers the point of the change: an ordinary user, on an instance licensed Ultimate, reading ultimate from both routes.
These were run, against a development environment on this branch (Ruby 3.3.11, PostgreSQL 17, Redis 7.2, Gitaly built): the four above are 48 examples, 0 failures, 4 pending, and the CE model spec is 1 example, 0 failures. The four pending ones are the granular-token shared examples excusing themselves ("namespace has no top-level group", "only meaningful on Project/Group boundaries"), not anything of this change.
The first run was not green, and both things it caught are worth naming rather than quietly fixing:
ee/spec/models/ee/app_config/instance_metadata_spec.rbfailed on theModel disables STIshared example this directory injects into every model spec. That example runs in the root group, where mybeforeblock read alicensethat only existed as aletinside each context, so it raisedundefined local variable or method 'license'. Fixed with a root-level default; the spec is now proof against any other example injected there.graphql-verifyfailed because a new field changes what the schema introspects to, and the two committed introspection schemas had not been regenerated. They now are, throughgitlab:graphql:generate_all_introspection_schemas, and the diff is exactly the twelve lines of this field in each.
bundle exec rake gitlab:graphql:check_docs reports "GraphQL documentation is up to date", so the doc/api/graphql/reference/_index.md row is byte-for-byte what the generator produces. Both OpenAPI documents were updated by hand to the shape the Grape entity produces; if bin/rake gitlab:openapi:v2:generate disagrees in any detail, the generated output is correct and mine should be replaced by it.
What this does not cover is the rest of the suite: only these five files were run locally, and the pipeline is what speaks for everything else.
MR acceptance checklist
- I have evaluated the MR acceptance checklist for this MR.
- Tests added for the new behaviour, covering CE, a licensed instance, an unlicensed instance, and the per-namespace case.
- Documentation updated:
doc/api/metadata.md, the GraphQL reference, and both OpenAPI documents. -
Changelog: addedtrailer on the commit. - Specs executed locally: 48 examples, 0 failures across the four that need a database, plus the CE model spec. See the note above for the two things the first run caught.
- Generated artifacts regenerated rather than hand-written where a generator exists (the two introspection schemas; the GraphQL reference verified with
check_docs).