Bridge UI
Bridge is a backend-for-frontend HTTP server that runs on your machine as the kubectl bridge
plugin. It exposes CRUD over the GitLabCore custom resource (apps.gitlab.com/v2alpha1) and
serves a single-page application (SPA) to configure GitLab instances. The Operator in the cluster
reconciles what Bridge writes.
This page describes how to prepare a cluster, grant access, and run the plugin. Without a source checkout, start at Install from a release. For what Bridge does, see Bridge API and kubectl bridge plugin. For the internal architecture and how to work on the code, see internal/bridge/AGENTS.md.
Authentication model
The plugin builds its Kubernetes client from your kubeconfig, so it authenticates the same way
kubectl does. Client certificate, exec, OIDC, and token kubeconfigs all work. You do not create a
service account, mint a token, or paste anything into the UI.
The Kubernetes API server authenticates and authorizes each call as your kubeconfig identity, never
as the Operator. An action you cannot perform receives a 403 response. Your identity therefore
needs RBAC on gitlabcores.apps.gitlab.com, on siphons.apps.gitlab.com for the Siphon add-on,
and on orbits.apps.gitlab.com for the Orbit add-on. For why Bridge works this way, see
ADR 34 and
ADR 23.
The plugin performs no authentication of its own. Anyone who reaches the port acts with your kubeconfig permissions. It binds loopback by default. You can bind a routable address, for example to run the plugin in a container, but the plugin prints a warning because that exposes full cluster access to the network.
The plugin answers 403 to an /api request whose Host is neither a loopback name, such as
localhost, 127.0.0.1, or [::1], nor the bind address, nor one you passed to --accept-hosts,
and to one that a browser marks as cross-site. The SPA, the docs UI, and clients such as curl are
unaffected. To reach the plugin under another name, such as a DNS entry or a container host name,
pass that name to --accept-hosts.
Install from a release
Each release tag publishes what you need to try Bridge without a source checkout:
- The Operator image with the v2alpha1 reconcilers,
bridge-staging:<version>. - The v2alpha1 definitions and the manager RBAC, which the Helm chart does not ship.
- The
kubectl bridgeplugin.
None of them is part of the release itself. For where CI publishes them, see
Bridge artifacts of a release. Take all of them from the same
<version>, because the plugin, the definitions, and the reconcilers must agree on the API. A
release without a bridge-staging:<version> image predates these artifacts.
Install the definitions and the RBAC before the chart. With the reconcilers enabled, the manager fails on start until the cluster serves the v2alpha1 definitions.
Set the version and the location of the published files:
VERSION=<version> PACKAGE="https://gitlab.com/api/v4/projects/18899486/packages/generic/kubectl-bridge/${VERSION}"Install the v2alpha1 definitions:
kubectl apply -f "${PACKAGE}/v2alpha1-crds.yaml"Grant the manager the permissions of the v2alpha1 reconcilers. The file names the installation with placeholders. Replace them with the chart value
nameOverride,gitlabby default, and the namespace you install the Operator into. The binding can name the manager ServiceAccount before the chart creates it:curl -fsSL "${PACKAGE}/v2alpha1-rbac.yaml" \ | sed -e "s/NAME_OVERRIDE_PLACEHOLDER/gitlab/g" -e "s/NAMESPACE_PLACEHOLDER/gitlab-system/g" \ | kubectl apply -f -Applied without the replacement, the binding is rejected, but the role is created. Delete it with
kubectl delete clusterrole NAME_OVERRIDE_PLACEHOLDER-manager-v2alpha1-role.Install the Operator chart of that version with the bridge image, and enable the reconcilers:
helm install gitlab-operator oci://registry.gitlab.com/charts/charts.gitlab.io/release/gitlab-operator \ --version "${VERSION}" \ --create-namespace \ --namespace gitlab-system \ --set image.repository=gitlab-org/cloud-native/gitlab-operator \ --set image.name=bridge-staging \ --set image.tag="${VERSION}" \ --set bridge.enabled=trueInstall the plugin for your platform:
linuxordarwin, onamd64orarm64:curl -fLO "${PACKAGE}/kubectl-bridge-darwin-arm64.tar.gz" curl -fLO "${PACKAGE}/SHA256SUMS" shasum -a 256 --check --ignore-missing SHA256SUMS tar -xzf kubectl-bridge-darwin-arm64.tar.gz kubectl-bridge mv kubectl-bridge /usr/local/bin/ # any directory on PATHContinue with Grant access and Start the plugin.
To move to another version, apply the definitions and the RBAC of that version, then upgrade the
chart with the same values. helm upgrade does not update them.
Build the Operator with the v2alpha1 reconcilers
The reconcilers of GitLabCore, Siphon, Orbit, and GitLabSecrets are gated behind the
bridge Go build tag, so they are absent from public operator images. CI publishes images
built with the tag to their own repository,
registry.gitlab.com/gitlab-org/cloud-native/gitlab-operator/bridge-staging: <branch-ref-slug>
on branch and merge request pipelines, latest on the default branch, and <version> on release
tags. A release never links a bridge image. See Container image tags.
Build a -bridge image locally with the dedicated task, or with the BUILD_TAGS build argument of
the Dockerfile:
CONTAINER_CLI=docker task docker-build-bridge # tags <image>:<TAG>-bridge
# or: docker build --build-arg BUILD_TAGS=bridge -t <image>:<tag>-bridge .For a local operator process, build with the tag:
go build -tags bridge -o bin/manager ./cmd/managerEnable the v2alpha1 reconcilers
Enabling requires a build with the tag (above); ENABLE_BRIDGE has no effect in a public image.
Set ENABLE_BRIDGE=true, which the operator reads in
controllers/settings/settings.go.
To enable them in the cluster, deploy a
-bridgeimage and set the chart valuebridge.enabled=true. The manager Deployment then injectsENABLE_BRIDGE. Install the v2alpha1 custom resources first, because the manager fails on start until the cluster serves them.export HELM_CHARTS=$(pwd)/charts CHART_VERSION=$(head -n1 CHART_VERSIONS) IMG_REPOSITORY=gitlab-org/cloud-native/gitlab-operator IMG_NAME=bridge-staging TAG=latest \ ARGS='--set bridge.enabled=true' task deploy_operatorTo enable them locally, set the variable when you run the tagged binary:
export HELM_CHARTS=$(pwd)/charts CHART_VERSION=$(head -n1 CHART_VERSIONS) ENABLE_BRIDGE=true go run -tags bridge ./cmd/managerA local run has no chart keys mounted, so a
GitLabCoreat a version thatHELM_CHARTSdoes not carry fails to pull. To pull it anyway, turn verification off withDYNAMIC_CHART_VERIFY=false, or pointDYNAMIC_CHART_KEYRINGat an exported key. For more information, see Chart provenance verification.
To create an instance in a namespace other than the Operator’s own, run the Operator in cluster
mode. The chart value watchCluster defaults to true, and task deploy_operator sets it from
CLUSTER_MODE, which also defaults to true. In namespaced mode, the Operator watches only its
own namespace and never reconciles an instance in another one. A chart before 10.5 also needs the
ServiceAccount of the Operator in the namespace of the instance, for its shared secrets Job.
Install the v2alpha1 custom resources
The apps.gitlab.com/v2alpha1 resources that
ADR 26 designs are not part of the Helm chart.
The chart is what produces the release manifests and the OLM bundle, so leaving them out keeps them
out of everything a user installs.
Install them in a development cluster:
task install_v2alpha1_crdsThe task applies what scripts/v2alpha1_manifests.sh writes, the same files a release publishes.
Without a checkout, apply those as Install from a release shows.
The task acts on the current kubectl context. Check it first, because a development cluster can
hold a real GitLab instance. It reads $NAMESPACE and $NAME_OVERRIDE to name the manager
ServiceAccount in the RBAC binding, so use the same values you deploy the Operator with. The task
needs no installed chart.
The task adds three definitions, each with v2alpha1 as its only version:
| Definition | Kind |
|---|---|
gitlabcores.apps.gitlab.com | GitLabCore |
orbits.apps.gitlab.com | Orbit |
siphons.apps.gitlab.com | Siphon |
task crd-docs generates the reference of these resources, doc/bridge/crd-reference.md, from the
doc comments in api/v2alpha1. Run it after you change those types.
GitLabCore is a definition of its own, not a second version of GitLab. A definition carries one
kind across all of its versions, so a differently named kind needs a definition of its own. The
existing gitlabs.apps.gitlab.com definition is untouched, keeps v1beta1 as its only served and
stored version, and needs no conversion webhook.
Nothing converts a GitLab into a GitLabCore. Kubernetes converts only between versions of one
definition, so an instance created through GitLab stays there. To work with the new resource,
start from the
GitLabCore sample.
Bridge reads and writes GitLabCore, Siphon, and Orbit, so install the definitions before you
use it. Without them, every API call returns an error from the Kubernetes API server.
GitLabCore, Siphon, and Orbit are reconciled only in a build with the bridge tag and
ENABLE_BRIDGE=true. For more information, see the GitLabCore reconciler.
Grant access
Bind your kubeconfig identity to a role with the verbs you need on GitLabCore, Siphon, and
Orbit resources. This example grants full CRUD to the user jane. For a read-only caller, drop
create, update, patch, and delete.
kubectl create clusterrole gitlab-editor \
--verb=get,list,watch,create,update,patch,delete \
--resource=gitlabcores.apps.gitlab.com,siphons.apps.gitlab.com,orbits.apps.gitlab.com
kubectl create clusterrolebinding jane-gitlab-editor --clusterrole=gitlab-editor --user=janeThe Namespace dropdown list of the form lists namespaces, and Create creates a new one.
Grant list and create on namespaces. Namespaces are cluster-scoped, so the grant needs a
ClusterRole and a ClusterRoleBinding:
kubectl create clusterrole gitlab-namespaces --verb=list,create --resource=namespaces
kubectl create clusterrolebinding jane-gitlab-namespaces --clusterrole=gitlab-namespaces --user=janeWithout list, you type the namespace name instead of choosing it. Without create, you can still
choose an existing namespace, and Create answers 403 for a new one.
To limit access to a single namespace, use a Role and RoleBinding instead of the cluster-scoped
variants, and skip the namespace grant.
Run Bridge
Install the plugin
Build and install the binary. The task builds the SPA first, then runs go install, which compiles
the SPA into the binary with go:embed:
task install-kubectl-pluginThe binary goes where go install puts it: $GOBIN when set, otherwise $(go env GOPATH)/bin. The
task prints the resolved path.
That directory must be on your PATH, because kubectl discovers plugins by searching PATH for
executables named kubectl-<name>. Add it to your shell profile if needed:
export PATH="$(go env GOPATH)/bin:$PATH"Confirm kubectl found the plugin:
kubectl plugin list | grep kubectl-bridgeTo build the binary into bin/kubectl-bridge without installing it, use task build-kubectl-plugin.
To install a prebuilt binary of a release instead, see Install from a release.
Start the plugin
Run the plugin against your current kubeconfig context:
kubectl bridgeThe plugin prints the identity it acts as and the URL, then opens the URL in your browser. The
header shows Local kubeconfig identity. For the API documentation, open /docs on the same
URL. Stop the plugin with Control+C.
The instance list refreshes itself every 10 seconds while its tab is visible, so an install or an upgrade can be followed without a reload. An open form refreshes the status panels of its add-ons, never its fields. Each open, visible list costs the API server one list request every 10 seconds, made as your kubeconfig identity. For the details, see Auto-refresh of instance status.
Plugin flags
| Flag | Default | Description |
|---|---|---|
--port, -p | 8090 | Local port to bind. Falls back to a random free port when the port is busy. |
--address | 127.0.0.1 | Address to bind. IPv6 literals such as ::1 work. A non-loopback address is allowed, with a warning. |
--accept-hosts | Loopback names | Comma-separated Host header names to accept on /api besides loopback ones and the bind address. Needed to reach the plugin under another name. |
--context | Current context | Kubeconfig context to use. |
--kubeconfig | Standard loading rules | Path to the kubeconfig file. |
--no-open | Off | Print the URL instead of opening a browser. |
--verbose, -v | Off | Log startup details and every served request at debug level. |
For example, to use a different context on another port without opening a browser:
kubectl bridge --context staging --port 9000 --no-openConfigure a GitLab instance
For what the form guarantees, see GitLab instance form.
The version starts on Latest with no major, which installs the newest chart version the Operator
finds. A Static chart version is free-form: a version the Operator does not bundle is pulled from
its chart repository when it starts reconciling, so the SPA has no fixed list of versions to
prefill from. The Upgrade of the list offers the versions the Operator reports in
status.upgradeVersions, so it lists what the Operator installs, behind a mirror too. While the
UpgradeVersionsListed condition is not True, the list shows Newer versions unknown rather
than Latest version. An unattended instance shows the newest listed version its type does not
move to, such as the first minor of the next major, as X available.
The form in the SPA writes one GitLabCore resource. A sidebar lists its sections, and selecting
one shows its fields:
- Basics: what the instance is, and which chart deploys it.
- Dependencies: the data stores the instance connects to.
- Networking: how the instance is reached. Manual mode turns the Gateway API off (the Operator does not install the chart’s bundled Envoy Gateway), leaving networking to the overrides.
- Core Components: which GitLab components the instance runs, one switch each. A switch left at
the chart default writes nothing to
spec.components. - Add-ons: the GitLab subcomponents the instance has. A new instance has none. Select +
beside Add-ons to open a dialog that describes every add-on and what it needs. Select one
to add it: it is switched on and opened. Adding Orbit adds Siphon too. The dialog marks
the add-ons the instance has as Added.
An add-on is switched off with the checkbox in its section, and stays listed until you leave the
page. Secret Manager, Container Registry, and Toolbox write
spec.components, and keep their settings while switched off. Siphon and Orbit write resources of their own, described in Configure the Siphon add-on and Configure the Orbit add-on. The dialog marks the add-ons that are not built yet as Soon. Editing an instance lists the add-ons it has, switched on or off. - Overrides: the chart values, for everything the sections above do not cover.
Moving between sections checks nothing, so you can look at another one with a half-filled section behind you. A section with unsaved changes carries a dot, and one that needs attention a red mark. Create and Save check every section and open the first one that does not hold up.
Each field maps to the specification:
| Field | Resource field | Description |
|---|---|---|
| Namespace | metadata | Namespace of the resource, fixed after creation. A dropdown list searches the namespaces you may list, and creates a new one on Create, before the instance. It hides the namespaces of Kubernetes, OpenShift, and common add-ons. Nothing is chosen at first. A namespace other than the Operator’s own needs the Operator in cluster mode. For more information, see Enable the v2alpha1 reconcilers. |
| Name | metadata | Name of the resource, fixed after creation. |
| Domain | spec.domain | Domain the instance is served under, such as example.com. GitLab is deployed at the subdomain gitlab.<domain>, and the form shows the host it gets as you type. |
| Edition | spec.edition | ee for Enterprise Edition, which runs the Free feature set until a license activates more, or ce for Community Edition. Defaults to ee. |
| License | spec.license.secretRef | Name and key of the Secret that holds the license. The license key itself never reaches Bridge or the resource. Leave both empty to run without a license. The form shows this group for Enterprise Edition only, and sends no license for Community Edition. |
| PostgreSQL | spec.postgresql | Hostname of the database server, and the Secret that holds the password of the database user. For the versions and extensions GitLab requires, see the PostgreSQL requirements. |
| Valkey | spec.redis | Hostname of the Valkey server, and the Secret that holds its password. Redis works in its place, and the resource and the chart values both still call the field redis. For the versions GitLab requires, see the Redis requirements. |
| Object storage | spec.objectStorage | Name and key of the Secret that holds the object storage connection. Setting it turns the consolidated object storage on, which the chart needs: artifacts, LFS, uploads, and packages are enabled with no connection of their own. The registry, Pages, and backups keep their own settings in the chart values. |
| Networking | spec.networking | Mode select: Manual (turns the Gateway API off, leaving networking to the chart values below), Envoy Gateway controller (recommended), GatewayClass, or Gateway. The latter two also take an optional “configure Envoy Gateway extensions” toggle, and the first two an optional cert-manager Issuer/ClusterIssuer reference. |
| Version | spec.version | A type and the one field it takes. Static runs the free-form Chart version; the Operator pulls a version it does not carry rather than failing, and an upgrade is a change of it. A Static version read from a ConfigMap (versionRef) is shown and kept until a version is typed in its place. Patch follows the latest patch of the Minor (x.y). Latest, Previous, and OldestSupported follow N, N-1, and N-2, counted across majors, within the Major. Latest is the default. Left empty, the Major is the one the instance runs, and before the install the one the count lands on. Every type but Static upgrades the running instance unattended, and a version below the deployed one is refused. |
| Chart values | spec.chart.values | Free-form YAML for everything the fields above do not cover. |
All three connections are required. The chart bundles neither PostgreSQL nor Redis, and it enables object storage for artifacts, LFS, uploads, and packages with no connection of its own, so an instance that leaves a group empty and does not configure it in the chart values fails to render.
The chart values are merged over the values the reconciler derives from the structured fields, and win on conflict. Use them as an escape hatch, and prefer a structured field when one exists.
A group is all or nothing: fill every field of PostgreSQL, Redis, or the license, or leave the group empty. The form reports an incomplete group before it sends the request.
Bridge rejects a value the definition would reject, such as a domain that is not a valid domain
name, with a 422 response that names the field. The constraints are part of the OpenAPI document,
so the generated client carries them too.
Configure the Siphon add-on
Siphon streams change data capture from the database of an instance into ClickHouse, through NATS
JetStream. Unlike the other fields of the form, it is a Siphon custom resource of its own, linked
to the instance through spec.gitlabRef. For more information, see
the Siphon reconciler.
In the SPA, select + beside Add-ons, then Siphon. Adding it selects Enable
Siphon. The panel configures one Siphon resource:
| Field | Resource field | Description |
|---|---|---|
| Chart version | spec.version.version | Version of the Siphon chart, which is not the GitLab chart. Required unless spec.version.versionRef is set, which the form shows and keeps until a version is typed. Only a version the Operator bundles renders, because a Siphon chart is never pulled. |
| PostgreSQL source | spec.source | Hostname, port, database, login role, password Secret, TLS mode, and advisory lock ID of the server the stream reads. It must be the primary. |
| NATS queue | spec.queue | URL of the server, the Secrets holding the user name and password, and the Secret holding a client certificate. Leave the credential fields empty for a server that accepts anonymous clients. |
| ClickHouse sink | spec.sink | Hostname, native protocol port, database, user, password Secret, and TLS of the server the stream is written to. |
| Table definitions | spec.tables | Where the table definitions come from, an image that overrides the one the GitLab version resolves to, and the Secret that image is pulled with. |
The three servers are referenced, not created. The publication, the siphon_alter_publication
function, the login roles and the grants on PostgreSQL, the NATS server, and the ClickHouse target
tables are all prerequisites. For the DDL, see the Siphon reconciler.
Bridge names the resource after the instance, such as gitlab-siphon, and finds it again
through its reference. A Siphon created with kubectl under another name is the one the form
edits from then on. A name longer than the 31 characters the definition allows is rejected with a
422 response.
Saving the form writes the instance first, then the Siphon. Clearing Enable Siphon on an
existing Siphon shows what the deletion leaves running, and saving deletes the resource only once
its name is typed. If the Siphon cannot be written, the instance is still saved. The
form stays open on the add-on, and saving again updates that instance and retries the Siphon. The
form does not create the instance a second time.
If the caller cannot read Siphon resources, for example because their role covers only
gitlabcores.apps.gitlab.com, the instance can still be edited and saved. The add-on is locked,
its section says why, and saving leaves any existing Siphon untouched.
Deleting a Siphon stops the pipeline and leaves its PostgreSQL publication and replication slot
and its NATS stream behind. Nothing reads the retained slot, so PostgreSQL keeps write-ahead log
for it until the volume of the source server is full, and then stops accepting writes. Drop the
slot right after the deletion, as Delete a pipeline describes. The
panel reports all three names while the resource exists. A deprovisioning runbook from Siphon is
requested in Siphon issue 350.
Configure the Orbit add-on
Orbit, the GitLab Knowledge Graph, indexes the datalake a Siphon fills into a graph database and
serves it to GitLab. It is an Orbit custom resource of its own. The add-on wires the instance,
its Siphon, and the Orbit together, so no reference is a field of the form:
spec.gitlabRefof theOrbitnames the instance.spec.siphonRefof theOrbitnames the Siphon of the instance.spec.orbitRefof theGitLabCorenames theOrbit, so GitLab connects to it.
For the reconciler and its prerequisites, see the Orbit reconciler.
In the SPA, select + beside Add-ons, then Orbit. Adding it adds Siphon too, if
the instance has none yet, and selects Enable Orbit and Enable Siphon. The panel configures
one Orbit resource:
| Field | Resource field | Description |
|---|---|---|
| Chart version | spec.version.version | Version of the Orbit chart. Required unless spec.version.versionRef is set, which the form shows and keeps until a version is typed. Only a version the Operator bundles renders. |
| ClickHouse datalake | spec.datalake | HTTP interface port, user, password Secret, and TLS that Orbit reads the Siphon datalake with. The host and database come from the Siphon. |
| ClickHouse graph | spec.graph | Hostname, HTTP interface port, database, writer user and password Secret, an optional read user and password Secret for the webserver, and TLS. |
| GitLab connection | spec.jwtSecretRef, spec.tls | The Secret holding the JWT key GitLab and Orbit share, and the TLS Secret the webserver serves gRPC with. |
The form refuses Orbit without Siphon, and refuses to turn Siphon off while Orbit stays on. Bridge
names the resource after the instance, such as gitlab-orbit, as it does a Siphon.
Saving writes the instance, the Siphon, then the Orbit. Clearing Enable Orbit disconnects
the instance and deletes the Orbit after a confirmation, before any Siphon deletion. Bridge
refuses to delete a Siphon that an Orbit references, so deleting a Siphon also needs list on
orbits.apps.gitlab.com. When the Orbit or the Siphon cannot be read, the add-on is locked and
saving leaves any existing Orbit untouched.
The panel reports the phase, the chart version, the GitLab version, and the gRPC endpoint of the
Orbit. The Orbit reports an endpoint only with a TLS Secret set, and until it does, GitLab is not
connected to it. The GitLab connection row shows the OrbitConnected condition of the instance:
Connected, or the reason and message that say why not.
Deleting an Orbit rolls GitLab out without the Orbit settings, and leaves the graph database and
the NATS streams in place.
Verify the RBAC delegation
To confirm Bridge acts as your kubeconfig identity rather than as the Operator, run the plugin with
a context whose identity lacks a verb. For example, a read-only identity that attempts a create
receives a 403 response:
kubectl bridge --context read-only --no-open
# In another shell, a read succeeds.
curl localhost:8090/api/v1/gitlabs
# A create the identity cannot perform returns HTTP 403.Frontend development
To work on the SPA with hot-module reload, run the plugin so :8090 is reachable, for example with
go run ./cmd/kubectl-bridge --no-open. Then start the Vite dev server:
task frontend-dev # http://localhost:5173, proxies /api, /openapi*, and /docs to :8090For more information about the frontend workflow and regenerating the OpenAPI document and typed client, see internal/bridge/AGENTS.md.