Skip to content

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_ENGINE is Django's signed_cookies backend 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"] from spec.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 raises OfflineGenerationError and 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.keystoneEndpoint keeps 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 the HORIZON_SECRET_KEY environment variable, and never rendered into the settings ConfigMap. The validating webhook rejects a SECRET_KEY entry in spec.extraConfig.
  • Static assets: built at image-build time. collectstatic --noinput and compress --force run 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 of nix/base-requirements.txt and operationally identical to the keystone deployment; uWSGI loads openstack_dashboard.wsgi directly. No per-CR uWSGI knobs in v1.
  • Login-page probes. The readiness and startup probes GET /auth/login/ with a fixed Host: localhost header, which satisfies Django's ALLOWED_HOSTS check 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 in upper-constraints.txt; overrides/<release>/constraints.txt removes the pin so the source install builds the ref from source-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:

ResourceNamePurpose
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 ​