POC: Create initial read-only mode UI for top-level groups
We need to explore feasibility/options for delivering a minimal read-only mode for TLGs.
A suggestion from @abdwdd was:
I'd suggest using a middleware to check if the TLG is in maintenance mode, and return that from the database.
We could use a namespace state management field to check for maintenance mode.
We should discuss how best to quickly demo this functionality and add middleware logic to check for this field.
- If a top-level group is marked as
read-only, this should be exposed on all routes - If the TLG is read-only, all web views should return a generic error page with a message about temporary maintenance
- All API routes (and GraphQL calls) for this group should return a generic error (503 or similar)
- Ideally, this logic can be extended to later achieve the same outcomes for a read-only Organization
The purpose of this POC is to determine feasibility for the above – we should be able to determine how complex the remaining work will be.
It should also expose what remaining parts are left to do, eg. areas where writes happen independently of HTTP requests from users.
Proposed Implementation Plan
Scope note (per @abdwdd feedback): Since this is a POC, the scope is cut to essentials. Admin toggle (Rake task) and background write path auditing are deferred to follow-up work. Maintenance mode will be toggled via Rails console. The goal is a single MR that demonstrates feasibility (does not need to merge to master).
Why "Namespace" and not "TLG"?
The implementation plan references "namespace" throughout because in the GitLab codebase, Group inherits from Namespace (class Group < Namespace). A top-level group (TLG) is a Namespace with no parent. The existing state management infrastructure (Namespaces::Stateful, effective_state, state_metadata) all live on the Namespace model, which is the correct abstraction layer to build on.
By implementing at the Namespace level, the maintenance/read-only mode automatically applies to TLGs and all their child namespaces (subgroups, projects) via effective_state ancestor traversal. This also positions the work for future Organization extensibility, since Organization has has_many :namespaces and could trigger maintenance on all its root groups, or adopt the same Stateful concern directly.
Existing Infrastructure (Codebase Analysis)
The codebase already has significant building blocks:
-
Namespaces::Stateful(app/models/concerns/namespaces/stateful.rb) already definesmaintenance: 6as a state value, but no state machine transitions exist for it yet -
Gitlab::Middleware::ReadOnly(lib/gitlab/middleware/read_only.rb+lib/gitlab/middleware/read_only/controller.rb) already blocks write HTTP methods (POST/PATCH/PUT/DELETE) at the instance level, with an allowlist pattern -
Namespace::Detail(app/models/namespace/detail.rb) already hasstate_metadata(jsonb) for tracking transition metadata -
effective_stateinStateQueryingalready traverses ancestor hierarchies to resolve inherited state
Implementation Phases & Dependency Graph
Step 1: State machine transitions
Step 2: Namespace-scoped read-only middleware ┐
Step 3: Web UI error page ├── all depend on Step 1; run in parallel
Step 4: GraphQL mutation enforcement ┘
All steps will be delivered in a single POC MR. Maintenance mode toggling will use the Rails console directly (no Rake task needed for the POC).
Step 1: State Machine Transitions for maintenance — #591688
Add enter_maintenance and exit_maintenance events to Namespaces::Stateful. The maintenance: 6 value exists but has no transitions. Follow the existing schedule_deletion / cancel_deletion pattern including state preservation. Add maintenance to FORBIDDEN_ANCESTOR_STATES.
Effort: Small (1-2 days) | Priority: P0
Step 2: Namespace-Scoped Read-Only Middleware — #591689 (closed)
Create Gitlab::Middleware::NamespaceReadOnly that blocks write HTTP requests for namespaces in maintenance state. Extract namespace from request path, check effective_state, return 503. Cache state lookups to avoid per-request DB hits.
Effort: Medium (3-5 days) | Priority: P0 | Depends on: Step 1
Step 3: Web UI Error Page — #591690 (closed)
Create app/views/errors/namespace_maintenance.html.haml for browser requests, and structured 503 JSON responses for API requests. Integrated into the middleware from Step 2.
Effort: Small (1 day) | Priority: P0 | Depends on: Step 1 | Parallel with: Steps 2, 4
Step 4: GraphQL Mutation Enforcement — #591691 (closed)
Block GraphQL mutations targeting namespaces in maintenance. Middleware alone is insufficient since all GraphQL is POST to /api/graphql with namespace in the body. Enforce at the mutation layer.
Effort: Medium (2-3 days) | Priority: P1 | Depends on: Step 1 | Parallel with: Steps 2, 3
Deferred to Follow-Up (Post-POC)
These items are out of scope for the POC but tracked for future work:
- Admin Toggle (Rake task / UI) — #591692 (closed): Create a proper Rake task and/or admin UI to trigger maintenance mode transitions. For the POC, Rails console is sufficient.
-
Background Write Path Audit & Guards — #591693 (closed): Audit Sidekiq workers, cron jobs, and PG triggers that write to namespaced data independently of HTTP requests. Create
Namespaces::MaintenanceGuardconcern.
Open Questions
- Caching strategy: Redis with short TTL vs per-request DB check vs request-store cache?
-
Git operations: Should Git reads (clone/fetch) still work? Instance-level maintenance allows
git-upload-packbut blocksgit-receive-pack— same pattern likely applies. -
Organization extensibility: Should
Organizationmodel eventually use the sameStatefulconcern? -
Child namespace state propagation: Rely on
effective_stateancestor traversal (implicit) vs updating childstatecolumns (explicit)?