Skip to content

Horizon CRD ​

horizons.horizon.openstack.c5c3.io/v1alpha1, kind Horizon. The CRD is generated from operators/horizon/api/v1alpha1/horizon_types.go; the Helm chart ships a synced copy (make sync-crds / make verify-crd-sync).

Spec ​

FieldTypeRequiredDescription
deploymentDeploymentSpecnoShared pod-level knobs: replicas (default 3), resources (resolved per resource when the pod is rendered: 70m CPU request, no CPU limit, and 720Mi memory request and limit for the fixed two uWSGI processes of one thread, see the resource defaults), terminationGracePeriodSeconds, preStopSleepSeconds, strategy, topologySpreadConstraints, priorityClassName, and the node placement nodeSelector, tolerations and affinity (see NodePlacementSpec). Horizon runs no Job, so it has no spec.jobs block
deployment.verticalAutoscaling*VerticalAutoscalingSpecnoOpts the Horizon dashboard Deployment into a VerticalPodAutoscaler that controls the requests of its containers; see VerticalAutoscalingSpec. Rejected beside spec.autoscaling, which scales the same Deployment. On a cluster without the VPA, VPAReady turns False with reason VPANotInstalled.
imageImageSpecyesContainer image; exactly one of tag or digest (shared CEL rule)
cacheCacheSpecyesMemcached backing the Django cache. Exactly one of clusterRef (managed) or servers (brownfield); backend is a Django cache backend path, defaulted to django.core.cache.backends.memcached.PyMemcacheCache. The operator renders CACHES["default"]["KEY_PREFIX"] as horizon- plus the first 12 hex characters of the SHA-256 of the image reference (repository:tag or repository@digest), so Horizon releases that share one Memcached never read each other's template-fragment cache and an image change starts with a cold cache. The prefix follows the spec and the operator never resolves the running digest: two builds pushed behind one tag share a prefix
keystoneEndpointstringyesThe Keystone endpoint URL (OPENSTACK_KEYSTONE_URL); must match ^https?:// and parse with a host. Consumed server-side by the dashboard pods, so it must be reachable from inside the cluster — for a colocated control plane use the cluster-local Service URL, not an externally routable address
secretKeyRefSecretRefSpecyesSecret holding the Django SECRET_KEY; key defaults to secret-key. Injected as the HORIZON_SECRET_KEY env var, never into the ConfigMap
gateway*GatewaySpecnoExternal exposure via a Gateway API HTTPRoute on port 8080; requires hostname and parentRef.name
networkPolicy*NetworkPolicySpecnoIngress restricted to TCP 8080 from the listed sources; egress auto-derived (DNS, the Keystone endpoint port, cache ports). At least one ingress source is required (fail-closed)
autoscaling*AutoscalingSpecnoHPA bounds, CPU/memory utilization targets and scaling behavior
logging*LoggingSpecnoDjango LOGGING dictConfig derivation: root level, debug, perLoggerLevels. Defaulted to text/INFO/debug: false
extraConfigmap[string]JSONnoFree-form Django settings rendered after the operator defaults (user values win). SECRET_KEY is rejected by the webhook. Overrides of operator-owned settings (for example STATIC_ROOT, COMPRESS_OFFLINE, or CACHES, whose override drops the image-scoped KEY_PREFIX) are honored but reported via the ExtraConfigHealthy condition and an ExtraConfigOwnedKeyOverride Warning event
websso*WebSSOSpecnoFederated single-sign-on choices on the login page. When nil the operator renders no WEBSSO_* settings and the dashboard offers local credentials only. The c5c3 ControlPlane projects this from the federation backends attached to its Keystone child
multiDomain*MultiDomainSpecnoMulti-domain login: the domain field (or dropdown) on the login form, needed when users live in domains other than the default one — typically LDAP-backed. When nil the operator renders no OPENSTACK_KEYSTONE_MULTIDOMAIN_* settings
secretStoreRef*SecretStoreRefSpecnoSelects the External Secrets store the dashboard resolves its SecretsReady gate against — kind (ClusterSecretStore | SecretStore, default ClusterSecretStore) and a required non-empty name. When omitted the shared cluster-scoped ClusterSecretStore openbao-cluster-store is used, so existing deployments are unchanged; set to a namespaced SecretStore in this Horizon's own namespace to reach OpenBao as a per-tenant identity. It only gates SecretsReady on the selected store — Horizon creates no PushSecrets. Normally projected from the owning ControlPlane
targetClusterRef*TargetClusterRefSpecnoNames the registered target cluster that receives this Horizon's children: the Deployment, the ConfigMaps, and the Secrets. The CR itself does not move, and neither do its status or the webhooks that admit it. When omitted the children are created on the local cluster the operator runs on, so an existing CR keeps its behavior without an edit. name must be a non-empty DNS-1123 subdomain matching the registration Secret. Immutable (CEL transition rule): adding, removing, or renaming the ref strands the children already created on the previously selected cluster; delete and recreate instead. See Target Clusters

Defaulting and validation ​

The mutating webhook applies the shared DeploymentSpec/LoggingSpec defaults, materializes the PyMemcacheCache backend, and defaults secretKeyRef.key. The validating webhook accumulates every violation: replicas floor, image tag/digest XOR, cache mutual exclusivity, the keystoneEndpoint URL shape, gateway hostname/parentRef, network-policy ingress, autoscaling bounds (including the implicit minReplicas default from deployment.replicas, and a non-zero spec.deployment.resources request for each utilization target), logging enums, graceful-termination cross-field arithmetic, topology-spread selectors, PriorityClass existence, and the spec.deployment.nodeSelector label grammar and tolerations (the API server's toleration rules).

A CEL rule over extraConfig keys is not expressible (the API server cannot build CEL type information for preserve-unknown-fields map values), so the empty-key and SECRET_KEY guards are webhook-only.

The validating webhook also holds spec.image.tag to a release floor when the tag parses as an OpenStack release; a digest-only image or a tag that names no release, such as latest, is skipped. A tag below 2026.1, the oldest release this operator version supports, is rejected on create and whenever an update changes it, with must be 2026.1 or later: this operator version no longer supports OpenStack releases below 2026.1. An update that keeps a stored tag below the floor is admitted with a warning, so an unrelated edit never blocks the resource: spec.image.tag "2025.2" is below 2026.1, the oldest OpenStack release this operator version supports; the unchanged value is admitted, but the operator renders the 2026.1 configuration for it. Set spec.image.tag to 2026.1 or later.

ExtraConfig ownership guard ​

Beyond the rejected settings, the operator ships a registry of the local_settings.py settings it computes. Overriding one of them through spec.extraConfig is report-only — the value is rendered — but it flips the informational ExtraConfigHealthy condition to False (Reason=OwnedKeysOverridden, with the overridden settings named in the message; with no overrides the condition reads True, Reason=NoOwnedKeysOverridden) and emits a one-shot ExtraConfigOwnedKeyOverride Warning event gated on that message transition — it fires once on entering False and once more when the overridden-setting set changes, never on the steady reconcile poll. The condition is intentionally not aggregated into Ready, so an explicit operator override is honored rather than blocking the rollout. OPENSTACK_ENDPOINT_TYPE and SECURE_PROXY_SSL_HEADER are deliberately left unregistered as supported escape hatches, so overriding either stays silent. Horizon has no separate events reference page by design; this is the documentation home for the ExtraConfigOwnedKeyOverride event.

WebSSOSpec ​

Maps onto Horizon's WEBSSO_* Django settings. Rendered by the settings renderer, so operators never hand-write Python literals into extraConfig.

The figure shows where these settings act. keystoneURL is the address of hop 2, and hop 8 uses spec.keystoneEndpoint. The hops of a login lists all eight.

A federated login through Horizon in eight numbered hops. Hop 1: the browser opens the login page of Horizon through the Gateway and picks an identity provider, and Horizon answers with a redirect. Hop 2: the browser follows it to the websso path of that provider on the public Keystone URL and passes the dashboard origin. The Service of Keystone sends every request to the federation-proxy container of the Keystone pod on port 5050, which redirects a browser without a session to the identity provider. Hop 3: the browser opens the authorization endpoint of the identity provider, and the user logs in there. Hop 4: the identity provider sends the browser back to /v3/OS-FEDERATION/redirect_uri on the public Keystone URL, with a code. Hop 5: the proxy exchanges the code at the token endpoint of the identity provider, a call that leaves the pod. Hop 6: the proxy passes the request to Keystone on 127.0.0.1:5000 with the claims as request headers, and Keystone answers with a form that carries a token, if the origin is a trusted dashboard. Hop 7: the browser posts that form to the origin, the path /auth/websso/ of Horizon. Hop 8: Horizon validates the token against the cluster-local Keystone URL and starts the session. Hops 1 to 4 and 7 are requests of the browser and use public names; hops 5 and 8 are calls between servers and use names a pod resolves. A SAML backend differs in hops 4 and 5: the browser posts the assertion to the proxy, and no call to the identity provider follows.

FieldTypeRequiredDefaultDescription
enabledboolnofalseTurns the SSO selector on the login page on (WEBSSO_ENABLED). When false the remaining fields are inert
choices[]WebSSOChoiceno—Ordered entries of the "Authenticate using" dropdown (WEBSSO_CHOICES); order is preserved verbatim. Required (non-empty) when enabled is true. The defaulting webhook prepends the local-credentials fallback when no choice carries the id credentials, which is what stops enabling SSO from locking out non-federated accounts. Max 17 counts the list after that prepend — the 16 federated entries idpMapping bounds, plus the fallback — so a list that already declares credentials may hold 17 entries and one that does not is bounded at 16. Submitting a 17th without the fallback is rejected by the defaulting webhook, because mutating admission runs before schema validation and a prepend would otherwise be rejected on a count you never wrote
idpMappingmap[string]WebSSOIDPTargetno—Maps a choice id onto the Keystone identity provider and federation protocol (WEBSSO_IDP_MAPPING). A choice with no mapping entry is a local login. Every key must name a declared choice. Max 16
initialChoicestringnocredentialsPreselects one of choices by id (WEBSSO_INITIAL_CHOICE). Must name a declared choice
keystoneURLstringno""The browser-facing Keystone base URL the SSO redirect is built from (WEBSSO_KEYSTONE_URL); matches ^https?://. It exists because spec.keystoneEndpoint is by contract the cluster-local Service URL, consumed server-side — redirecting a browser there would fail to resolve. When empty the setting is omitted and Horizon falls back to spec.keystoneEndpoint

WebSSOChoice ​

FieldTypeRequiredDescription
idstringyesSubmitted as the form's auth_type value and referenced by idpMapping / initialChoice. Matches ^[A-Za-z0-9_.-]+$ (it round-trips through a URL query string)
labelstringyesHuman-readable text rendered for this choice

WebSSOIDPTarget ​

FieldTypeRequiredDescription
identityProviderstringyesThe Keystone identity-provider id (the backend's effective identityProviderName)
protocolstringyesThe Keystone federation protocol id (e.g. openid)

Operator-pinned WebSSO settings ​

Two settings are rendered by the operator and are not configurable through spec.websso:

  • WEBSSO_USE_HTTP_REFERER = False. It defaults to True upstream, which makes openstack_auth validate the returned token against the Keystone URL derived from the browser's Referer — i.e. the external gateway URL, resolved server-side from inside the pod, where it does not reach Keystone. With False it validates against OPENSTACK_KEYSTONE_URL instead.
  • SECURE_PROXY_SSL_HEADER = ["HTTP_X_FORWARDED_PROTO", "https"], rendered when spec.gateway is set. A Gateway terminates TLS and forwards plain HTTP, so without it Django reports http:// and the origin it sends Keystone never matches an https:// trusted_dashboard. Gating on the Gateway is what makes trusting the header safe: Envoy overwrites X-Forwarded-Proto on ingress.

MultiDomainSpec ​

Configures the login form's domain handling. Horizon only renders the domain dropdown when domainDropdown is true and domainChoices is non-empty; with enabled alone the form shows a free-text domain field.

FieldTypeRequiredDefaultDescription
enabledboolnofalseTurns on multi-domain login (OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT)
defaultDomainstringnoDefaultThe domain assumed for users who supply none (OPENSTACK_KEYSTONE_DEFAULT_DOMAIN). Materialized by the defaulting webhook
domainDropdownboolnofalseReplaces the free-text domain field with a select populated from domainChoices (OPENSTACK_KEYSTONE_DOMAIN_DROPDOWN). Requires enabled and a non-empty domainChoices
domainChoices[]DomainChoiceno—Ordered entries of the domain dropdown (OPENSTACK_KEYSTONE_DOMAIN_CHOICES). Rendered only when domainDropdown is true. Max 32

DomainChoice ​

FieldTypeRequiredDescription
namestringyesThe Keystone domain name submitted with the login form
labelstringyesHuman-readable text rendered for this domain

Status ​

FieldDescription
conditionsList-map keyed by type; see the reconciler reference for the vocabulary
observedGenerationThe .metadata.generation last reconciled
endpointThe dashboard URL: https://{gateway.hostname}/ when a gateway is set, otherwise the cluster-local Service URL

Sub-Resource Naming Convention ​

Operator-managed sub-resources (Deployment, Service, PodDisruptionBudget, HorizontalPodAutoscaler, NetworkPolicy, HTTPRoute) use the bare CR name with no suffix, matching the keystone convention. A Horizon CR named horizon in the openstack namespace is therefore reachable in-cluster at horizon.openstack.svc.cluster.local:8080 — the Service DNS name is the CR name. The immutable settings ConfigMap is the one derived name: {name}-config-<content-hash>.

Example ​

yaml
apiVersion: horizon.openstack.c5c3.io/v1alpha1
kind: Horizon
metadata:
  name: horizon
  namespace: openstack
spec:
  deployment:
    replicas: 3
  image:
    repository: ghcr.io/c5c3/horizon
    tag: "2026.1"
  cache:
    clusterRef:
      name: openstack-memcached
  keystoneEndpoint: http://keystone.openstack.svc.cluster.local:5000/v3
  secretKeyRef:
    name: horizon-secret-key
  gateway:
    parentRef:
      name: openstack-gw
      namespace: envoy-gateway-system
    hostname: horizon.127-0-0-1.nip.io