Generated at build time from
charts/racora-controller/README.md(helm-docs:values.yaml+README.md.gotmpl); edit the source, not this page.
racora-controller
The Racora NRCell controller — a kopf-based K8s operator that reconciles NRCell CRDs into:
- DU Deployments + ConfigMaps + Services in
distributed-unit - CU-CP mobility config overlay (
nrcell-cu-cp-configConfigMap incentralized-unit) - NRCell status (controller-assigned identity —
gnbId,sectorId,gnbDuId,identitySource— plus NCI, pciSource, lastRetune, conditions)
Requires
racora-crds(NRCell CRD)racora-base(racora-systemanddistributed-unitnamespaces)
Install
helm install racora-controller ./charts/racora-controller
Values
| Key | Type | Default | Description |
|---|---|---|---|
| namespace | string | "racora-system" | Namespace this controller is deployed into. Must already exist (racora-base creates it). |
| duNamespace | string | "distributed-unit" | Where the controller writes generated DU resources (Deployments, ConfigMaps, Services). Must already exist (racora-base creates it). |
| cuNamespace | string | "centralized-unit" | Where the controller writes the cu-cp mobility overlay ConfigMap (nrcell-cu-cp-config). This MUST match the namespace where cu-cp is deployed — the DU and CU are separate planes with separate knobs. |
| crdNamespace | string | "racora-system" | Where the controller watches for NRCell CRs. Today the controller watches a single namespace; this aligns with how kopf is invoked in main.py (kopf run ... --namespace=racora-system). |
| coreNamespace | string | "5g-core" | Namespace where the bundled 5G core (Open5GS) runs. The controller derives the CU-CP's default AMF address from it (amf. |
| ueNamespace | string | "user-equipment" | Namespace where UE simulator pods (srsUE) run. Used as the default ZMQ rx_port host when an NRCell with ruType=zmq doesn't override spec.radioBackend.zmq.rxPort. |
| gnbId | int | 411 | CU-wide gNodeB ID. One gNB per CU in the current software, so every cell shares this value — the controller stamps it into each cell's status and uses it for every NCI (NCI = gnbId << 14 |
| image.pullPolicy | string | "IfNotPresent" | Image pull policy — IfNotPresent, because the images ship with the distribution. The image itself is the umbrella's global.components.controller. |
| mplaneHttpPort | int | 5055 | TCP port of the mplane sidecar's HTTP endpoints (its /ru/ready is the DU's startup gate). The DU pod runs on the host network, so the port is the node's: keep it clear of anything else on the radio's node. |
| o1.enabled | bool | true | O1 on every element: a netconf sidecar (the O1 NETCONF server, global.components.netconf, seeded from the config the controller renders) and an o1 sidecar (the O1 adapter in the element's role) next to each gNB container, which reads the config the adapter renders from the datastore. Off: the gNB reads the controller's ConfigMap directly. |
| o1.httpPort | int | 5056 | TCP port of the o1 sidecar's HTTP endpoints (/status, /config-healthy, the gNB container's liveness) in every element pod; on an ofh DU's host network it is the node's. |
| o1.netconfPort | int | 830 | The NETCONF port every element serves (SSH), on the element's Service; on an ofh DU's host network it is the node's. |
| o1.accountSecret | string | "o1-netconf-account" | The Secret (username, password) with the NETCONF account every element serves, generated by this chart into the DU and CU namespaces and the controller's own, where the controller mounts it. |
| o1.accountUsername | string | "racora" | The username of that account. |
| o1.hostkeySecret | string | "o1-hostkey" | The Secret (ssh_host_ecdsa_key) with the SSH host key every element's NETCONF server presents, generated by this chart into the DU and CU namespaces. |
| o1.checkSeconds | int | 30 | Seconds between the controller's readings of every element's O1 datastore: a cell's O1Ready and O1Config conditions, the snapshot it keeps in o1-snapshot-<element>, and the merge-back of what was added over O1 after a pod starts again. |
| o1.ves.url | string | "http://o1-collector.racora-smo.svc.cluster.local:8080" | The VES collector's base URL for the adapters the controller launches when this chart is installed on its own; under the umbrella global.o1.ves.url wins. |
| o1.pm.streamTarget | string | "http://o1-collector.racora-smo.svc.cluster.local:8080/pm/stream" | Where the racora-default PerfMetricJob streams performance data when this chart is installed on its own; under the umbrella global.o1.pm.streamTarget wins. |
| oruResource | string | "racora.io/oru" | The device-plugin resource the fronthaul agent (racora-node) advertises per O-RU; an ofh DU with an M-plane address requests one, so it schedules only where its O-RU is up. Empty when the agent is not deployed (nodePlugins.fronthaul.enabled=false), or no ofh DU schedules. |
| ofhUnreachableGraceSeconds | int | 90 | Seconds the O-RU may be unhealthy under a running DU (RadioReady=False/Unreachable) before the controller replaces the DU pod; the replacement waits until the agent reports the O-RU healthy, then provisions it again. |
| duServedCheckSeconds | int | 30 | Seconds between the controller's checks of which DUs the CU-CP serves over F1 (its metrics stream), written to every cell as DUServed. |
| duUnservedGraceSeconds | int | 180 | Seconds a ready DU may stay unserved by the CU-CP (DUServed=False/NotServed) before the controller replaces its pod; a fresh DU attaches over F1 like a new cell. |
| mobility.triggerHandoverFromMeasurements | bool | true | Automatic A3/measurement-driven handover at the CU-CP. CU-wide and boot-only in OCUDU: the controller bakes it into the CU-CP overlay, so a change takes effect at the next cu-cp restart — which is why it lives here and not on the NRCell spec. |
| mobility.resyncIntervalS | int | 300 | Cadence (seconds) of the idempotent upsert-only mobility re-push to the running CU-CP — belt-and-braces convergence for edge cases the per-reconcile runtime sync can miss. |
| cucpMetricsPeriodMs | int | 5000 | CU-CP metrics report period (ms), baked into the CU-CP overlay. Without it the CU-CP emits no periodic RRC/NGAP counter lines (handover requested/successful etc.) and the monitoring dashboards starve. Boot-only: takes effect at the next cu-cp restart. |
| decisions.pollIntervalS | int | 1 | Seconds between /decisions/active polls (per NRCell timer). |
| decisions.minConfidence | float | 0.95 | Minimum decision confidence to act on (deterministic functions emit 1.0; load-bearing once learned models emit decisions). |
| decisions.minStabilizationS | int | 30 | Seconds a cell's last applied change (per function) must age before the next decision for that function may apply. |
| decisions.perFunction | object | {} | Per-function overrides of the two knobs above, keyed by function name ('pci', 'anr'). |
| replicaCount | int | 1 | Single replica — kopf-based reconciler, no HA. Scale-out would require leader election which kopf supports but the controller isn't wired for it yet. |
| livenessProbe.enabled | bool | true | Serve kopf's health endpoint from the operator's event loop and probe it: a controller whose event loop stops (a deadlock, a hung call) stops answering and the kubelet restarts it. |
| livenessProbe.port | int | 8080 | Port of the health endpoint (/healthz), inside the controller pod. |
| livenessProbe.initialDelaySeconds | int | 30 | Seconds after start before the first probe. |
| livenessProbe.periodSeconds | int | 20 | Seconds between probes. |
| livenessProbe.timeoutSeconds | int | 5 | Seconds a probe may wait for the answer. |
| livenessProbe.failureThreshold | int | 3 | Failed probes in a row before the kubelet restarts the controller. |
| resources | object | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} | Resource requests/limits. The controller is light: kopf event loop + small in-memory state per NRCell. Defaults sized for ~50 cells. |
| extraEnv | list | [] | Extra env vars (e.g., for the CU-IP Flight URL override). Each entry is rendered into the container's env list verbatim. |
| controlPlanePin | bool | true | Pin both reconcilers (racora-controller, racora-core-controller) to the control node, like the CU planes and the in-cluster core: "first node is the brain", additional nodes are DU/RF capacity that comes and goes with the radios. Adds node-role.kubernetes.io/control-plane=true to the nodeSelector; platforms with tainted control planes set it false and pin with nodeSelector instead (platforms/kubernetes/values.yaml). |
| nodeSelector | object | {} | Node selector for the controller pod. |
| tolerations | list | [] | Tolerations for the controller pod. |
| affinity | object | {} | Affinity rules for the controller pod. |
| serviceAccount.create | bool | true | Create the ServiceAccount, ClusterRole and binding the controller needs (cross-namespace ConfigMaps, Deployments and Services; NRCell status). |
| serviceAccount.name | string | "" | ServiceAccount name; empty = the chart-generated default. |
| coreController.enabled | bool | true | Deploy racora-core-controller (the Subscriber reconciler and core provider adapter). |
| coreController.image.pullPolicy | string | "IfNotPresent" | Image pull policy — IfNotPresent, because the images ship with the distribution. The image itself is the umbrella's global.components.coreController. |
| coreController.resyncSeconds | int | 300 | Seconds between resyncs that re-apply every Subscriber to the core (the provider's apply is an upsert, so a wiped core database heals). |
| coreController.retrySeconds | int | 30 | Seconds before a Subscriber whose provider was not ready (or whose adapter failed) is retried. |
| coreController.resources | object | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} | Resources for the core controller: a light kopf loop. |
Verify
kubectl get pods -n racora-system -l app=racora-controller
kubectl logs -n racora-system -l app=racora-controller --tail=20
# Apply a test NRCell to confirm reconciliation (see the repo README
# for a copy-pasteable example cell):
kubectl get nrcell -n racora-system
kubectl get deployment -n distributed-unit
RBAC
The controller's ClusterRole grants cross-namespace writes for
ConfigMaps + Deployments + Services. Today scoped cluster-wide for
simplicity; in a tighter security posture this could be narrowed to
just distributed-unit + centralized-unit via RoleBindings.
The core controller
The same chart deploys racora-core-controller, the core-side reconciler: a
second, small kopf service owning the Subscriber CRD. Each Subscriber is
provisioned into the selected core provider through the adapter the provider
declares (the racora-core-provider ConfigMap in coreNamespace; see
cores/README.md), retried while the provider is not ready, and re-applied on
a cadence (coreController.resyncSeconds) so a wiped core database heals. Its
RBAC is separate: Subscribers, the credentials Secrets in crdNamespace, and
pods/exec in coreNamespace — the RAN controller holds none of those.