The Orbit reconciler

internal/controller/orbit reconciles the apps.gitlab.com/v2alpha1 Orbit resource, the GitLab Knowledge Graph. For what it does, see the Orbit reconciler spec. For the design, see ADR 36.

To configure Orbit in the Bridge UI instead of writing the resources by hand, see Configure the Orbit add-on.

An Orbit references the GitLabCore it belongs to and the Siphon whose datalake and NATS stream it indexes. It points at a graph database and at Secrets. It provisions none of them. Create the prerequisites below first.

Enable the reconciler

The reconciler is gated like the Siphon reconciler:

  • The bridge build tag compiles it in. Without the tag, orbit_stub.go takes its place.
  • ENABLE_BRIDGE=true registers it at runtime.

Run task install_v2alpha1_crds to install the definitions and grant the manager ServiceAccount what the reconciler needs.

Run task retrieve-charts to fetch the Orbit chart at the version in ORBIT_CHART_VERSIONS into charts/. The chart is pulled from oci://registry.gitlab.com/gitlab-org/orbit/orbit-helm-charts/orbit. The gkg chart, version 3.x and older, is not supported.

The Orbit image tag comes from the chart, which pins image.tag to its appVersion. Set spec.chart.values.image.tag to run another build.

Name the resource

Name the resource with at most 44 characters. The chart names each indexer pool <name>-orbit-indexer-<pool> and cuts that name at 63 characters, so 44 keeps the default pools, sdlc and code, whole. A name that already ends in orbit is not suffixed again.

A pool name of your own that is longer than four characters needs a shorter resource name. When two pools are cut to the same name, the Orbit reports RenderFailed and names the object, rather than applying one pool over the other.

Create the prerequisites

GitLab and Siphon

Deploy Orbit with GitLab chart 10.4 (GitLab 19.4) or later, the oldest chart the Operator configures Orbit for. Orbit itself needs GitLab 19.2.2 or later, with a Premium or Ultimate license applied. Against an older instance, the Orbit reports GitLabVersionUnsupported. The Operator does not check the license.

Configure ClickHouse in the GitLabCore through global.clickhouse in spec.chart.values. The GitLab migrations then create the datalake tables the Siphon writes to and Orbit reads:

yaml
spec:
  chart:
    values:
      global:
        clickhouse:
          enabled: true
          main:
            url: http://clickhouse.databases.svc:8123
            database: gitlab_clickhouse_main_production
            username: gitlab
            password:
              secret: clickhouse-gitlab
              key: password

Create a Siphon in the same namespace that names the same GitLabCore. For more information, see the Siphon reconciler. The Orbit reads the NATS connection, the datalake host and database, and the stream name from it.

The Orbit chart takes no NATS credentials. A Siphon that sets spec.queue.auth is refused with SiphonQueueAuthUnsupported. Use a client certificate in spec.queue.tls instead.

ClickHouse

Orbit connects to ClickHouse over the HTTP interface, port 8123. The native protocol port, 9000, is rejected.

Create the users, the roles, and the graph database from clickhouse-setup.sql, which ships in the Orbit image at /usr/share/gkg/clickhouse-setup.sql and in the Orbit repository. It creates three users:

UserFieldAccess
gkg_siphon_readerspec.datalake.userReads the datalake that the Siphon writes.
gkg_writerspec.graph.userCreates, migrates, and writes the graph.
gkg_readerspec.graph.readUserReads the graph for the webserver.

Add the three grants the file does not include. A managed ClickHouse usually restricts the system database, and without them the schema migrations stop when a new version is promoted:

sql
GRANT SELECT ON system.parts TO gkg_app;
GRANT SELECT ON system.tables TO gkg_app;
GRANT SELECT ON system.dictionaries TO gkg_reader_app;

The Operator refuses the clickhouseSetup hook of the chart, which runs this SQL with the admin password.

JWT key

GitLab and Orbit share one HS256 key. GitLab signs user tokens with it, and Orbit verifies them and signs its own calls to GitLab with it. Create 32 random bytes, base64 encoded, in a Secret:

shell
kubectl create secret generic gitlab-orbit-jwt --from-literal=secret="$(openssl rand -base64 32)"

Reference it from spec.jwtSecretRef of the Orbit. With spec.orbitRef, the GitLabCore mounts the same Secret into Rails, as the next section shows, so the two sides cannot disagree. The Operator checks that the Secret exists, and never reads the key.

Orbit and Rails read the key at startup. To rotate it, change the Secret, then restart the Orbit webserver and indexer and the GitLab webservice and Sidekiq.

TLS

GitLab connects to Orbit only over TLS. Create a kubernetes.io/tls Secret whose certificate covers the webserver Service, <name>-orbit-webserver.<namespace>.svc, and reference it from spec.tls.secretName. A name that already ends in orbit is not suffixed again, so an Orbit named orbit serves orbit-webserver.<namespace>.svc. Without spec.tls, terminate TLS in front of the webserver Service yourself.

GitLab must trust the CA that issued the certificate, and the Operator does not configure that. Use a certificate from a CA GitLab already trusts, or add the CA to the GitLabCore through global.certificates.customCAs in spec.chart.values.

Connect GitLab to Orbit

Name the Orbit in spec.orbitRef of the GitLabCore:

yaml
spec:
  orbitRef:
    name: orbit

The Operator then reads status.grpcEndpoint and spec.jwtSecretRef of the Orbit, and sets global.appConfig.orbit of the GitLab chart. The Operator supports Orbit on GitLab chart 10.4 or later. The OrbitConnected condition of the GitLabCore says whether it did, and why not:

ReasonMeaning
OrbitConfiguredGitLab is configured with the endpoint and the Secret.
OrbitEndpointPendingThe Orbit reports no endpoint. It reports one only with spec.tls set, once its release is applied.
OrbitSecretMissingThe JWT Secret does not exist. The Operator leaves the settings out, because the GitLab chart mounts it without the optional flag.
OrbitGitLabMismatchThe Orbit belongs to another GitLabCore.
OrbitNotFoundNo Orbit of that name exists in the namespace.
OrbitValuesConflictspec.chart.values set global.appConfig.knowledgeGraph, which the chart refuses beside orbit. Move it to orbit.
OrbitChartUnsupportedThe GitLab chart is older than 10.4.
OrbitUnreadableReading the Orbit or the Secret failed. While the failure is transient, such as a timeout, the status is Unknown and the pass is retried with the settings kept. A refused read, such as Forbidden, is False, and GitLab renders without the settings.

The reference never delays the GitLabCore. On a new installation GitLab therefore rolls out twice: once without the Orbit settings, and again once the Orbit reports its endpoint. Deleting the Orbit removes the settings and rolls GitLab out again. For the other drawbacks, see ADR 37.

When TLS is terminated in front of Orbit, the Orbit reports no endpoint. Leave spec.orbitRef unset and set the settings in spec.chart.values of the GitLabCore instead:

yaml
spec:
  chart:
    values:
      global:
        appConfig:
          orbit:
            enabled: true
            grpcEndpoint: tls://orbit.example.com:50054
            jwtSecret:
              secret: gitlab-orbit-jwt
              key: secret

Then turn indexing on for a top-level group on the GitLab Orbit configuration page, /admin/orbit.

What the Operator sets

The Operator derives the values below. spec.chart.values overrides each of them:

  • gitlab.baseUrl, as http://<gitlabRef>-webservice-default.<namespace>.svc:8181.
  • The ClickHouse connections, the Secret references, and the NATS connection.
  • schedule.tasks.siphon.events_stream_name, from status.streamName of the Siphon.
  • analytics.enabled: false. The chart turns Snowplow analytics on.

Replicas, indexer pools, and resources are not modeled. Tune them in spec.chart.values.

The Operator also forces values that spec.chart.values cannot change:

  • dispatcher.enabled: true. The dispatcher creates and migrates the graph tables, and the indexer waits for it.
  • webserver.enabled: true and indexer.enabled: true. Available waits for both. Which indexer pools run stays with spec.chart.values, but removing every pool is refused.
  • metrics.prometheus.podMonitor.enabled: false and alerts.enabled: false on a cluster that does not serve the PodMonitor or PrometheusRule kinds.

It refuses spec.chart.values that turn on any of the following, with ValuesRefused:

ValueReason
clickhouseSetup.enabledA pre-install hook that creates ClickHouse users with the admin password.
dashboards.enabledA post-install hook that creates ConfigMaps no release tracks, with RBAC.
healthCheck.enabledA Role in every namespace it targets. The Operator applies no RBAC.
indexer.keda.enabled, or the same key of a poolKEDA autoscaling requires the health-check.

Verify that Orbit runs

Available reports that the webserver, the dispatcher, and every indexer pool have rolled out. It does not report indexing progress. The first index finishes long after the workloads are ready.

A pass that waits or fails before it checks the workloads sets Available to Unknown, with reason NotChecked. Expect it while the Siphon runs its initial snapshot: the Siphon flaps not ready, and the Orbit waits with SiphonNotReady until it is back, leaving the release running.

Delete an Orbit

Deleting an Orbit removes what the chart rendered. Garbage collection does it, so the resource carries no finalizer. The graph database and the NATS streams that Orbit created stay in place.

End-to-end tests

task e2e-suite SUITE=orbit deploys the Operator as an image and drives a real Orbit through the API server. It needs no GitLab. It covers the schema, the wait for a missing instance, the version reference, the requeue, and deletion.

The waits on the Siphon reference follow an available GitLabCore. The unit tests in internal/controller/orbit cover them, and every render against the chart.

task e2e-suite SUITE=knowledgegraph runs an Orbit against a real GitLab, Siphon, NATS, and ClickHouse. It checks that the Orbit becomes available and its dispatcher creates the graph schema, and that a GitLabCore that references it through spec.orbitRef rolls out with the endpoint and the JWT Secret.