Backends and transports¶
You declare settings. The role decides which transport carries each one. This page explains that decision, so a skip or a routing failure reads as a statement about your configuration rather than a surprise.
A preference, not a switch¶
gitlab_settings_backend is a preference. Each operation is applied through
the first transport in that preference which is capable of the resource —
capability being a static, per-resource fact recorded in the registry, not
something discovered at runtime.
Two conditions must both hold for a transport to carry an operation:
- It is capable of that resource.
- Its channel is ready for the target — credentials present for the API transports, a reachable host or toolbox pod for Rails.
Routing never bypasses an unavailable preferred channel to silently use a different one. Changing transport can change which authority performs the work, so the role refuses rather than substituting.
Presets¶
| Value | Expands to | Use when |
|---|---|---|
api (default) |
[rest, graphql] |
Production automation. Protocol-agnostic: you never choose REST or GraphQL, only that a setting is applied. Pairs naturally with strict mode. |
all |
[rest, graphql, rails] |
You need Rails-only surfaces. API-capable operations still take their earlier API transport. Pairs naturally with compatible mode. |
A single transport (rest, graphql, rails) or an explicit ordered list
([rails, rest]) is also accepted. Order is significant — the first capable,
ready transport owns the operation.
The transports¶
=== "REST"
Normalizes desired state into ordered resource operations, then reconciles
each with read-before-write: full pagination, identity matching,
type-tolerant comparison, transient-failure retries, and real `--check` and
`--diff` support. Secret fields travel separately from displayable
operation data.
=== "GraphQL"
Reconciles the committed GraphQL-served resources — compliance frameworks,
on-call schedules, security policies, and others listed in the
[capability matrix](generated-capability-matrix.md). Operations run against
committed, schema-validated documents, never strings built at runtime.
=== "Rails"
Stages a role-owned bulk executor and one JSON document of pre-planned
operations on the GitLab host, then invokes `gitlab-rails runner` **once**
per apply — one Rails boot regardless of item count. See
[Rails execution](rails-execution.md).
REST versus GraphQL is invisible to you: the two API surfaces are interchangeable for the settings the role manages. Rails is always an explicit opt-in, because it executes code on the instance.
When an operation cannot be routed¶
An operation is unroutable when every transport capable of it is absent from your preference, or when the selected transport's channel is not ready.
What happens then depends on gitlab_settings_mode:
| Mode | Behaviour |
|---|---|
strict (default) |
The run fails, naming the operation and the reason. Strict success means every declared operation had a route to convergence. |
compatible |
The operation is recorded as one skip with an actionable reason, and the run continues. |
A skip reason names only transports — never a value from your settings. For
example: a resource capable only of rails when your preference is api.
The narrow escape hatch¶
gitlab_settings_allow_unavailable_transports: true permits a recorded
strict-mode skip in exactly one case: a transport you did select whose
channel is unavailable.
It does not permit omitting every capable transport from your preference. That remains a strict failure, because it means you asked for work you never enabled a way to perform.
Dependencies may cross partitions forward, never backward¶
Transport partitions apply in a fixed order — Rails, then REST, then GraphQL — with every check-mode preflight completing before any partition writes. A dependent operation may therefore rely on a parent in an earlier-applying partition: a group created over REST can own a GraphQL child (a custom emoji, an audit streaming destination) in the same apply. Only a plan that would run the parent after its dependent is rejected.
- Strict: such a plan fails rather than executing incompletely.
- Compatible: the dependent operation is skipped, and the skip propagates to anything depending on it.
The reason names the parent operation, its transport, and that its partition applies after the dependent's.
The coverage manifest¶
Every apply returns a coverage list binding each planned operation — by id,
resource, and target — to either the backend that processed it or its skip
reason. Nothing is silently dropped:
coverage:
- id: "1"
resource: group
target: platform
backend: rest
- id: "2"
resource: group_saml_provider
target: platform
skipped: true
reason: "requires the rails transport; add rails (or a preset that includes it) to backend"
Use it as the audit record of a run. In compatible mode it is also the list of what you still need to arrange a transport for.
Choosing a combination¶
| Situation | Backend | Mode |
|---|---|---|
| Production automation over the API | api |
strict |
| You need Rails-only surfaces and cannot fail the run | all |
compatible |
| Ephemeral lab, maximum surface | all |
compatible |
| Proving a specific transport works | explicit list | strict |
Strict mode is the default deliberately: a run that quietly skipped half your desired state, and reported success, is worse than one that stopped.
Related¶
- Reconciling settings — what a run does with the operations once routed
- Rails execution — the opt-in transport in detail
- Capability matrix — which transports serve which resources
- ADR-0002, ADR-0006, and ADR-0011 — why it works this way