Skip to main content

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-config ConfigMap in centralized-unit)
  • NRCell status (controller-assigned identity — gnbId, sectorId, gnbDuId, identitySource — plus NCI, pciSource, lastRetune, conditions)

Requires​

  • racora-crds (NRCell CRD)
  • racora-base (racora-system and distributed-unit namespaces)

Install​

helm install racora-controller ./charts/racora-controller

Values​

KeyTypeDefaultDescription
namespacestring"racora-system"Namespace this controller is deployed into. Must already exist (racora-base creates it).
duNamespacestring"distributed-unit"Where the controller writes generated DU resources (Deployments, ConfigMaps, Services). Must already exist (racora-base creates it).
cuNamespacestring"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.
crdNamespacestring"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).
coreNamespacestring"5g-core"Namespace where the bundled 5G core (Open5GS) runs. The controller derives the CU-CP's default AMF address from it (amf..svc.cluster.local). Must match racora-core's namespace. A customer core is addressed with the umbrella's global.core.amfAddr instead (env RACORA_AMF_ADDR), which overrides this.
ueNamespacestring"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.
gnbIdint411CU-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.pullPolicystring"IfNotPresent"Image pull policy — IfNotPresent, because the images ship with the distribution. The image itself is the umbrella's global.components.controller.
mplaneHttpPortint5055TCP 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.enabledbooltrueO1 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.httpPortint5056TCP 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.netconfPortint830The NETCONF port every element serves (SSH), on the element's Service; on an ofh DU's host network it is the node's.
o1.accountSecretstring"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.accountUsernamestring"racora"The username of that account.
o1.hostkeySecretstring"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.checkSecondsint30Seconds 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.urlstring"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.streamTargetstring"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.
oruResourcestring"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.
ofhUnreachableGraceSecondsint90Seconds 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.
duServedCheckSecondsint30Seconds between the controller's checks of which DUs the CU-CP serves over F1 (its metrics stream), written to every cell as DUServed.
duUnservedGraceSecondsint180Seconds 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.triggerHandoverFromMeasurementsbooltrueAutomatic 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.resyncIntervalSint300Cadence (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.
cucpMetricsPeriodMsint5000CU-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.pollIntervalSint1Seconds between /decisions/active polls (per NRCell timer).
decisions.minConfidencefloat0.95Minimum decision confidence to act on (deterministic functions emit 1.0; load-bearing once learned models emit decisions).
decisions.minStabilizationSint30Seconds a cell's last applied change (per function) must age before the next decision for that function may apply.
decisions.perFunctionobject{}Per-function overrides of the two knobs above, keyed by function name ('pci', 'anr').
replicaCountint1Single 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.enabledbooltrueServe 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.portint8080Port of the health endpoint (/healthz), inside the controller pod.
livenessProbe.initialDelaySecondsint30Seconds after start before the first probe.
livenessProbe.periodSecondsint20Seconds between probes.
livenessProbe.timeoutSecondsint5Seconds a probe may wait for the answer.
livenessProbe.failureThresholdint3Failed probes in a row before the kubelet restarts the controller.
resourcesobject{"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.
extraEnvlist[]Extra env vars (e.g., for the CU-IP Flight URL override). Each entry is rendered into the container's env list verbatim.
controlPlanePinbooltruePin 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).
nodeSelectorobject{}Node selector for the controller pod.
tolerationslist[]Tolerations for the controller pod.
affinityobject{}Affinity rules for the controller pod.
serviceAccount.createbooltrueCreate the ServiceAccount, ClusterRole and binding the controller needs (cross-namespace ConfigMaps, Deployments and Services; NRCell status).
serviceAccount.namestring""ServiceAccount name; empty = the chart-generated default.
coreController.enabledbooltrueDeploy racora-core-controller (the Subscriber reconciler and core provider adapter).
coreController.image.pullPolicystring"IfNotPresent"Image pull policy — IfNotPresent, because the images ship with the distribution. The image itself is the umbrella's global.components.coreController.
coreController.resyncSecondsint300Seconds between resyncs that re-apply every Subscriber to the core (the provider's apply is an upsert, so a wiped core database heals).
coreController.retrySecondsint30Seconds before a Subscriber whose provider was not ready (or whose adapter failed) is retried.
coreController.resourcesobject{"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.