Horizon Operator
The Horizon operator deploys and manages the OpenStack Dashboard as a Kubernetes-native workload. It is the second service operator built on the shared scaffolding the Keystone operator established (internal/common, the operator-library Helm chart, the parameterized operator image) and proves the onboarding path is repeatable.
Horizon's profile is deliberately the thin one among the service operators: a stateless Django/WSGI web application with no message bus, no service-catalog endpoints of its own, and none of Keystone's db-sync/fernet/bootstrap/upgrade machinery. What it needs instead: Memcached for the Django cache, the Keystone public endpoint, a shared Django SECRET_KEY, pre-built static assets, and — uniquely among the services — HTTP ingress by default.
Design decisions
The v1 operator resolves the onboarding decisions as follows:
- Sessions: signed cookies, no database.
SESSION_ENGINEis Django'ssigned_cookiesbackend and Memcached only backs the Django cache (CACHES,PyMemcacheCache). Losing Memcached degrades cache hit-rate; it neither logs users out nor flips any condition. The operator therefore has no database sub-reconcilers at all. DB-backed sessions are revisited only if signed cookies plus Memcached prove insufficient. - Cache keys: scoped per image. The operator derives
CACHES["default"]["KEY_PREFIX"]fromspec.image:horizon-plus the first 12 hex characters of the SHA-256 of the image reference. Horizon keeps its Angular template-cache preload in the Django fragment cache, and django-compressor hashes that fragment against the offline manifest built into each image. A fragment written by another release has no entry in that manifest, so every page render raisesOfflineGenerationErrorand the dashboard answers HTTP 500, the login page included. With the prefix, Horizon CRs on different images never read each other's entries. An image change starts with a cold cache, and two CRs on one image share a warm one (#1305). - Keystone endpoint: a plain URL field.
spec.keystoneEndpointkeeps the operator decoupled from the keystone-operator. The c5c3 ControlPlane operator derives the value top-down from its Keystone child's naming convention (never from the child's status — no machine consumer reads status endpoints). SECRET_KEY: ESO-managed Secret, env-var-injected. The key is required for multi-replica signed-cookie consistency, synced from OpenBao via an ExternalSecret (mirroring the keystone credential pattern), injected into the pods as theHORIZON_SECRET_KEYenvironment variable, and never rendered into the settings ConfigMap. The validating webhook rejects aSECRET_KEYentry inspec.extraConfig.- Static assets: built at image-build time.
collectstatic --noinputandcompress --forcerun in the image build against a throwaway settings file; uWSGI serves the result via--static-map. No runtime init container. See Container Images. - WSGI server: uWSGI. Pinned in the horizon lock under
nix/locks/from the base pins ofnix/base-requirements.txtand operationally identical to the keystone deployment; uWSGI loadsopenstack_dashboard.wsgidirectly. No per-CR uWSGI knobs in v1. - Login-page probes. The readiness and startup probes GET
/auth/login/with a fixedHost: localhostheader, which satisfies Django'sALLOWED_HOSTScheck without allow-listing the pod IP; rendering the page exercises Django's URL routing, the templates and the offline-compression manifest without a live Keystone. The liveness probe only opens a TCP connection to port 8080. The startup probe allows 300 seconds (30 probes 10 seconds apart, each with an 8-second timeout) before the liveness probe takes over, so a cold start slowed by a CPU limit set on the container or a contended node does not restart the container. - The
horizon===constraint pin is stripped. Unlike keystone, horizon is pinned inupper-constraints.txt;overrides/<release>/constraints.txtremoves the pin so the source install builds the ref fromsource-refs.yaml. - No events, no finalizer in v1. The operator emits no custom Kubernetes events (conditions carry the full state contract), and every owned resource is namespace-scoped and garbage-collected via ownerReferences, so deletion needs no finalizer pass. An events reference page is added when the first event lands.
Owned resources
The figure under Owned Resources of the Keystone operator draws the baseline this list follows: serving objects, config and Secrets, Jobs and CronJobs.
For a Horizon CR named {name} the operator manages:
| Resource | Name | Purpose |
|---|---|---|
| Deployment | {name} | The uWSGI dashboard pods (port 8080) |
| Service | {name} | ClusterIP in front of the dashboard pods |
| PodDisruptionBudget | {name} | minAvailable: 1 (or maxUnavailable: 1 at a lower replica bound of one, which is the HPA's minReplicas while spec.autoscaling is set) |
| ConfigMap | {name}-config-<hash> | Immutable, content-addressed local_settings.py |
| HorizontalPodAutoscaler | {name} | Only when spec.autoscaling is set |
| NetworkPolicy | {name} | Only when spec.networkPolicy is set |
| HTTPRoute | {name} | Only when spec.gateway is set |
Reference pages
- Horizon CRD — the full
spec/statuscontract - Reconciler Architecture — the sub-reconciler chain, conditions, and requeue semantics