Skip to content

Neutron CRD ​

neutrons.neutron.openstack.c5c3.io/v1alpha1, kind Neutron. The CRD is generated from operators/neutron/api/v1alpha1/neutron_types.go; the defaulting and validating webhooks live in operators/neutron/api/v1alpha1/neutron_webhook.go.

One CR describes the Neutron network service as this operator runs it: the API server behind uWSGI, the two worker Deployments that serve no HTTP, the database and cache connections, the Keystone integration, the message bus, and the recurring comparison of the Neutron database against the OVN Northbound database. The mechanism driver is ML2/OVN and nothing else. The rendered ml2_conf.ini sets mechanism_drivers = ovn, and the [ovn] connection strings come from the OVNCentral named by spec.ovn.centralRef. The CR carries one such reference, because the logical network model lives in one Northbound database.

The bus posture follows from that. Nothing in this deployment consumes RPC: OVN answers DHCP and metadata out of the logical model, no agent registers with the API, and no neutron-rpc-server is projected. The rendered configuration sets both RPC worker counts to zero and drops versioned notifications at the source. spec.messaging stays required, because every neutron process and every migration Job is started with OS_DEFAULT__TRANSPORT_URL sourced from the derived transport-URL Secret. See Rendered defaults.

The OVN metadata agent is a separate kind, NeutronMetadataAgent. It runs on the compute and network nodes, in the privileged namespace the OVNChassis DaemonSets live in, which is not a namespace the API Deployment belongs in.

kubectl get neutrons prints Ready (.status.conditions[?(@.type=='Ready')].status), Release (.status.installedRelease), Endpoint (.status.endpoint), and Age.

Most deployments get their Neutron from a ControlPlane projection. See ServiceNeutronSpec for which of the fields below the plane fills and which it leaves to this CR.

Spec ​

FieldTypeRequiredDefaultDescription
openStackReleasestring (Pattern ^\d{4}\.[12]$)yesnoneThe OpenStack release the operator deploys and drives. It governs install and upgrade release tracking: status.installedRelease is promoted to this value after a successful db-sync. The pattern admits the YYYY.N cadence with N in {1, 2}, the same class the validating webhook and release.ParseRelease accept, so a non-cadence minor is rejected at every layer. Kept separate from the image tag so a digest-pinned image still names a schema. The validating webhook rejects a value below 2026.1, the oldest supported release, on create and on change, and warns on an unchanged one.
deploymentcommonv1.DeploymentSpecno{}Pod-level knobs of the API Deployment: replicas (default 3), resources (resolved per resource when the pod is rendered: 70m CPU request, no CPU limit, and 720Mi memory request and limit at the default spec.apiServer.uwsgi counts, see the resource defaults), terminationGracePeriodSeconds, preStopSleepSeconds, strategy, topologySpreadConstraints, priorityClassName, and the node placement nodeSelector, tolerations and affinity (see NodePlacementSpec). It is also the fallback of spec.jobs
deployment.verticalAutoscaling*VerticalAutoscalingSpecnonilOpts the Neutron API 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.
jobs*commonv1.JobSpecnonilSizes, prioritizes and places the pods of the db-sync Job, the db-expand, db-migrate and db-contract upgrade phases, and the ovn-db-sync CronJob. A field left unset falls back to spec.deployment. Unset resources default to a 70m CPU request and 368Mi memory as request and limit; ovn-db-sync gets the request floor instead (see OVNDBSyncSpec)
imagecommonv1.ImageSpecyesnoneThe Neutron container image, run by the API pods, both worker Deployments, the migration Jobs, and the ovn-db-sync CronJob. tag and digest are mutually exclusive and one of the two is required. The field carries no immutability rule
databasecommonv1.DatabaseSpecyesnoneThe MariaDB connection, rendered into the plain [database] section. One of clusterRef (managed) or host (brownfield), never both, plus database, secretRef, and the optional port, credentialsMode and tls. credentialsMode: Dynamic requires clusterRef. replicas and storageSize sit in the schema and are read by the ControlPlane's managed-mode projection alone, so this operator ignores them
cachecommonv1.CacheSpecyesbackend: dogpile.cache.pymemcacheThe Memcached instance backing the keystonemiddleware token cache, rendered as [keystone_authtoken] memcached_servers. One of clusterRef (managed) or servers (brownfield), never both. Managed mode resolves to <clusterRef.name>:11211; replicas is honoured by the ControlPlane projection alone
keystoneEndpointstring (MinLength=1, Pattern ^https?://)yesnoneThe Keystone auth URL rendered as [keystone_authtoken] auth_url. keystonemiddleware validates every token against it server-side, so it has to be reachable from the Neutron pods: use the cluster-local Service URL, and not an address that only resolves outside the cluster
keystonePublicEndpointstring (Pattern ^https?://)nofalls back to keystoneEndpointThe browser-facing Keystone base URL rendered as [keystone_authtoken] www_authenticate_uri, the address a 401 points unauthenticated clients at. Resolved at render time by EffectiveKeystonePublicEndpoint, so a later edit of keystoneEndpoint keeps being tracked by the fallback
serviceUserServiceUserSpecyessee belowThe Keystone service account Neutron authenticates as, and the Secret holding its password
regionstringno""The Keystone region ([keystone_authtoken] region_name). When empty the option is omitted and Neutron uses the catalog's default region
apiServer*APIServerSpecnoniluWSGI process tuning. When nil the reconciler falls back to the same constants the webhook writes into a present block
workersWorkersSpecno{}Pod-level knobs of the two worker Deployments. Both are sized by the one block
messagingcommonv1.MessagingSpecyesreplicas: 3The RabbitMQ connection. One of clusterRef (managed) or secretRef (brownfield), never both, plus the optional tls block naming a CA bundle Secret. replicas is read by the ControlPlane's managed-mode projection alone. A placed Neutron whose bus runs on the management cluster has to use secretRef: the transport-URL helper reads and writes in the CR's own namespace through the CR's own children client, which for a placed CR is the target cluster's
ovnOVNSpecyesnoneThe OVN control plane this Neutron programs
nova*NovaSpecnonil (notifications off)The compute service this Neutron notifies when a port changes state, and the Keystone account it notifies as. The presence of the block is the switch: while it is set the operator turns both notify_nova_on_port_* flags on and renders the [nova] credentials, and while it is absent the flags stay off and the section stays empty, so a port never waits for a vif-plugged event nothing sends. There is no enabled field, and no endpoint override: neutron's [nova] option group carries region_name and endpoint_type alone, so the compute endpoint is resolved from the Keystone catalog
ovnDBSync*OVNDBSyncSpecnonil (no CronJob)The recurring neutron-ovn-db-sync-util run. A nil block means no CronJob at all. See ovnDBSync
gateway*commonv1.GatewaySpecnonilExternal exposure through a Gateway API HTTPRoute forwarding to the {name} Service on port 9696. Requires hostname and parentRef.name, and takes an optional path and annotations map. Removing the block deletes the HTTPRoute. It does not change status.endpoint, which stays the cluster-local Service URL
networkPolicy*commonv1.NetworkPolicySpecnonilIngress restricted to TCP 9696 from the listed sources; egress derived for DNS, the database, the Keystone endpoint, the cache, the two OVN databases, and the broker port, with additionalEgress appended after it. At least one ingress source is required (fail-closed)
autoscaling*commonv1.AutoscalingSpecnonilHPA bounds, CPU/memory utilization targets and scaling behavior for the API Deployment. At least one of targetCPUUtilization or targetMemoryUtilization is required, and an unset minReplicas resolves to spec.deployment.replicas. The two worker Deployments are never autoscaled
logging*commonv1.LoggingSpecnotext / INFO / debug: falseoslo.log derivation: format (text or json), level, debug, perLoggerLevels. Materialized by the defaulting webhook. The json format ships a logging.conf in the config ConfigMap and points [DEFAULT] log_config_append at it
secretStoreRef*commonv1.SecretStoreRefSpecnonil (openbao-cluster-store)Selects the External Secrets store the operator resolves SecretsReady against: kind (ClusterSecretStore | SecretStore, default ClusterSecretStore) and a required name. A namespaced store is resolved in this Neutron's own namespace. The ControlPlane projects this field onto the Neutron it owns
targetClusterRef*commonv1.TargetClusterRefSpecnonil (the local cluster)Names the registered target cluster that receives this Neutron's children: the three Deployments, the ConfigMaps, the Secrets, and the database CRs. The CR itself stays on the management cluster, and so do its status and its finalizers. Immutable, enforced by two CEL transition rules and by the webhook. See Target Clusters
extraConfigmap[string]map[string]stringnonilFree-form INI sections for options with no dedicated field. The render-time merge is operator defaults < extraConfig, so a user value wins, and each section is routed to the file its consumer reads. An override of an operator-owned key is honored and reported through the ExtraConfigHealthy condition and an ExtraConfigOwnedKeyOverride Warning event, except for the fifteen keys the webhook refuses outright. Option names are checked at admission against the per-release catalog embedded in the operator. See Defaulting and validation

DeploymentSpec, AutoscalingSpec, NetworkPolicySpec, NetworkPolicyIngressSource, LoggingSpec, GatewaySpec, GatewayParentRefSpec and UWSGISpec are Go aliases of the shared commonv1 definitions, which carry the per-field godoc and the validation markers.

ServiceUserSpec ​

The name and domain fields are webhook-defaulted, so a minimal CR need only supply the password Secret reference.

FieldTypeRequiredDefaultDescription
usernamestringnoneutronKeystone username ([keystone_authtoken] username)
projectNamestringnoserviceThe project the service user scopes to (project_name)
userDomainNamestringnoDefaultThe domain the service user lives in (user_domain_name)
projectDomainNamestringnoDefaultThe domain the service project lives in (project_domain_name)
secretRefcommonv1.SecretRefSpecyeskey → passwordThe Secret holding the service-user password. The password is injected as the OS_KEYSTONE_AUTHTOKEN__PASSWORD env var and never rendered into the config

Each of the four identity fields is written verbatim into [keystone_authtoken], so the validating webhook rejects a newline or carriage return in any of them. spec.region reaches the same section through the same renderer and goes through the same check.

NovaSpec ​

The notifier is a REST client of the compute API's os-server-external-events resource, so it needs a Keystone account of its own and a region to resolve the endpoint in.

FieldTypeRequiredDefaultDescription
regionstringno""The Keystone region the compute endpoint is resolved in ([nova] region_name). When empty the option is omitted and the notifier uses the catalog's default region
serviceUserNovaNotifierUserSpecyessee belowThe Keystone account the notifier authenticates as, and the Secret holding its password

NovaNotifierUserSpec ​

The field set of ServiceUserSpec with defaults of its own. The account is separate from spec.serviceUser because the notifier calls the compute API as a user holding the admin role, which the token-validation account does not need: nova resolves the instance behind a notified port with the caller's own context, unelevated, so a notifier holding service alone has every event answered 404 and leaves the port in BUILD.

FieldTypeRequiredDefaultDescription
usernamestringnoneutron-novaKeystone username ([nova] username)
projectNamestringnoserviceThe project the notifier account scopes to (project_name)
userDomainNamestringnoDefaultThe domain the notifier account lives in (user_domain_name)
projectDomainNamestringnoDefaultThe domain the notifier project lives in (project_domain_name)
secretRefcommonv1.SecretRefSpecyeskey → passwordThe Secret holding the notifier password. It is injected as OS_NOVA__PASSWORD into the API Deployment, both worker Deployments and the ovn-db-sync CronJob, and never rendered into the config. The webhook requires a non-empty name: serviceUser.secretRef.name must be set when spec.nova is configured: it carries the password Neutron notifies Nova with

The region and the four identity fields are written verbatim into [nova], so the validating webhook rejects a newline or carriage return in any of the five, the way it does for spec.serviceUser.

A CR written before spec.nova existed configures the notifier through spec.extraConfig: the two [DEFAULT] notify_nova_on_port_* switches and the [nova] section. Those keys are now operator-owned, so after the upgrade such a CR reports ExtraConfigHealthy=False with an ExtraConfigOwnedKeyOverride Warning event, and its next write raises an admission warning, although nothing it renders changes: spec.extraConfig still wins the merge. A carried-over [nova] password is kept with a warning (see Rejected owned keys). Move the account into spec.nova, with its password in the Secret serviceUser.secretRef names, and drop the entries from spec.extraConfig.

APIServerSpec ​

Tunes the API server process. It carries the uWSGI block alone: the API pods start uWSGI on the neutron.wsgi.api module, so there is no launch mode to select. The RPC worker counts live in [DEFAULT] and are fixed at zero.

FieldTypeRequiredDefaultDescription
uwsgi*commonv1.UWSGISpecnoniluWSGI application-server parameters: processes (default 2), threads (default 1), httpKeepAlive (default true), harakiri and httpKeepAliveTimeout (both omitted when nil). A CEL rule and the webhook agree that httpKeepAliveTimeout may only be set while httpKeepAlive is true

The concurrency this block declares also sizes the database. The SQL user's max_user_connections cap is (pods + 1) x processes x (5 + threads - 1), plus 2 x (worker replicas + 1) x 5, plus two transient Job connections; pods is the autoscaling ceiling when an HPA owns the replica count, and the + 1 on each term is the surge pod a rollout adds. The 5 is oslo.db's max_pool_size default, which the rendered [database] section does not override: a neutron process keeps up to that many pooled connections once load has touched it. A single-threaded API process measured 4 to 5 under plain port CRUD, and every uWSGI thread beyond the first adds one. Below that cap the last processes to start fail their pool with MySQL error 1226 and crash-loop their pod, and under load the process that opens the connection past it answers HTTP 500.

WorkersSpec ​

FieldTypeRequiredDefaultDescription
deploymentcommonv1.DeploymentSpecno{}Pod-level knobs of the worker Deployments. replicas (default 3) sizes both, so the default is three periodic-worker pods and three OVN maintenance-worker pods. Each worker runs one single-threaded process, so a resources block that names neither CPU nor memory renders a 70m CPU request, no CPU limit, and 368Mi as memory request and limit (see the resource defaults). The worker Deployments render its nodeSelector, tolerations and affinity too. The webhook validates this block like spec.deployment, except that topologySpreadConstraints must be unset or empty (see Webhook rules)
deployment.verticalAutoscaling*VerticalAutoscalingSpecnonilOpts both worker Deployments (periodic-workers, ovn-maintenance-worker) into a VerticalPodAutoscaler that controls the requests of its containers; see VerticalAutoscalingSpec. On a cluster without the VPA, VPAReady turns False with reason VPANotInstalled.

The block configures two Deployments, {name}-periodic-workers and {name}-ovn-maintenance-worker. Each selects its pods by its own component label and derives its default topology spread from that selector. A constraint set on the shared block could name only one of the two selectors, so the webhook forbids a non-empty list; an empty list switches the injected defaults off for both.

Neither worker Deployment gets a Service, an HPA or a PodDisruptionBudget: no client dials them, their load is the maintenance queue, and an eviction costs a delayed maintenance pass.

OVNSpec ​

FieldTypeRequiredDefaultDescription
centralRefOVNCentralRefyesnoneThe OVNCentral whose Northbound and Southbound databases the ML2/OVN mechanism driver connects to

OVNCentralRef ​

Unlike the reference on OVNChassis, this one carries a namespace: the OVN control plane commonly lives in the privileged networking namespace while the Neutron API lives with the rest of the control plane. The operator reads the connection details out of the central's status and mirrors its client Secret; the source is never mounted into the Neutron pods.

FieldTypeRequiredDefaultDescription
namestring (MinLength=1)yesnoneThe OVNCentral's name
namespacestringnothis CR's namespaceThe namespace the OVNCentral lives in. The defaulting webhook fills an empty value, so the CR records which namespace was meant when it was created and a later move of the Neutron cannot re-point it

Both fields are resolved into the [ovn] connection strings, so both are checked for control characters at admission.

OVNDBSyncSpec ​

Neutron and the OVN Northbound database each hold their own copy of the logical network model, and a write that fails halfway through leaves them apart. The utility walks both and reports, or repairs, the difference.

FieldTypeRequiredDefaultDescription
schedulestringno0 * * * *, resolved at reconcile timeThe standard cron expression the CronJob runs on. Checked by the validating webhook, with no CRD pattern behind it: the accepted grammar includes descriptors such as @daily, which no regex expresses without also rejecting valid expressions
syncModestring (Enum log, repair)nologWhat the utility does with the difference it finds
suspendboolnofalsePauses the CronJob without deleting it. The escape hatch for a maintenance window in which a repair-mode run would fight an operator editing the Northbound database by hand

The CronJob pod takes its priority class and node placement from spec.jobs, falling back to spec.deployment. Its resources resolve through the request floor rather than the Job defaults: a 70m CPU and a 256Mi memory request, and no limit. The utility loads both logical models, so its working set grows with them, and a default memory limit would OOM-kill the run once they outgrew it.

Rendered defaults ​

reconcile_config.go renders two files into one immutable ConfigMap, neutron.conf first and ml2_conf.ini second. The API Deployment names them in OS_NEUTRON_CONFIG_DIR and OS_NEUTRON_CONFIG_FILES, since uWSGI imports neutron.wsgi.api and there is no argv to carry a flag; the worker Deployments, the migration Jobs and the ovn-db-sync CronJob pass the same two files as --config-file arguments. The order is the same on both paths, so an ml2 option set in both files resolves to the ml2_conf.ini value. Section names decide the file. ml2, the five ml2_type_* sections, ovn, ovn_nb_global, ovs, ovs_driver, securitygroup and sriov_driver go to ml2_conf.ini; everything else goes to neutron.conf. The routing covers spec.extraConfig too, so a user override lands in the file its consumer reads.

Nothing here depends on spec.openStackRelease: two CRs differing only in their release render byte-identical files.

What a CR that says nothing beyond the required fields gets in neutron.conf:

SectionKeyValue
[DEFAULT]core_pluginml2
[DEFAULT]service_pluginsovn-router
[DEFAULT]auth_strategykeystone
[DEFAULT]api_paste_config/var/lib/openstack/etc/neutron/api-paste.ini
[DEFAULT]state_path/var/lib/neutron
[DEFAULT]rpc_workers0
[DEFAULT]rpc_state_report_workers0
[DEFAULT]dhcp_agent_notificationfalse
[DEFAULT]dns_domaincobaltcore.local.
[DEFAULT]notify_nova_on_port_status_changestrue while spec.nova is set, false otherwise
[DEFAULT]notify_nova_on_port_data_changestrue while spec.nova is set, false otherwise
[DEFAULT]use_stderrtrue
[DEFAULT]debugfrom spec.logging.debug, which oslo.log reads independently of the root logger level
[DEFAULT]default_log_levelsthe sorted spec.logging.perLoggerLevels CSV; omitted when the map is empty
[DEFAULT]log_config_append/etc/neutron/logging.conf; written only for spec.logging.format: json
[database]connectionmysql+pymysql://placeholder
[keystone_authtoken]auth_typepassword
[keystone_authtoken]auth_url, www_authenticate_uri, username, project_name, user_domain_name, project_domain_namefrom spec.keystoneEndpoint, the effective public endpoint, and spec.serviceUser
[keystone_authtoken]region_name, memcached_serversfrom spec.region and spec.cache; each omitted when its source is empty
[oslo_messaging_notifications]drivernoop
[oslo_messaging_rabbit]rabbit_quorum_queue, rabbit_transient_quorum_queue, use_queue_managertrue
[oslo_messaging_rabbit]ssl, ssl_ca_filetrue and /etc/rabbitmq-ca/ca.crt; written only while spec.messaging.tls is set
[oslo_concurrency]lock_path/var/lib/neutron/lock
[nova]auth_type, auth_url, username, project_name, user_domain_name, project_domain_namepassword, spec.keystoneEndpoint, and the four spec.nova.serviceUser identity fields; written only while spec.nova is set
[nova]region_namefrom spec.nova.region; omitted when empty
[nova]endpoint_typeinternal, the catalog entry a co-located control plane can reach. The public one routes the notifications out of the cluster and back
[nova]noneWithout spec.nova the section is rendered empty. The header is what a Nova option added through spec.extraConfig fills in

And in ml2_conf.ini:

SectionKeyValue
[ml2]mechanism_driversovn
[ml2]type_driversgeneve,flat
[ml2]tenant_network_typesgeneve
[ml2]extension_driversport_security,dns_domain_ports,tag_ports_during_bulk_creation
[ml2_type_geneve]vni_ranges1:65536
[ml2_type_geneve]max_header_size38, the room the chassis encapsulation reserves. A larger header than the tunnel accounts for exceeds the path MTU
[ml2_type_flat]flat_networks*
[securitygroup]enable_security_grouptrue
[ovn]ovn_nb_connection, ovn_sb_connectionthe two addresses resolved off the referenced OVNCentral
[ovn]ovn_nb_private_key, ovn_sb_private_key/etc/ovn/tls/tls.key
[ovn]ovn_nb_certificate, ovn_sb_certificate/etc/ovn/tls/tls.crt
[ovn]ovn_nb_ca_cert, ovn_sb_ca_cert/etc/ovn/tls/ca.crt
[ovn]ovn_l3_schedulerleastloaded, which spreads the gateway routers over the chassis carrying the gateway role
[ovn]ovn_metadata_enabledtrue, which is what makes OVN serve 169.254.169.254 to the instances the NeutronMetadataAgent answers for

Five keys carry the bus posture. rpc_workers and rpc_state_report_workers are both zero, because nothing consumes RPC here and no neutron-rpc-server is projected. [oslo_messaging_notifications] driver is noop, so versioned notifications are dropped at the source; nothing accumulates in a queue nobody drains. The two notify_nova_on_port_* options follow spec.nova: the port events are REST calls to os-server-external-events rather than oslo notifications, so the noop driver does not reach them. A valid transport_url is still needed: spec.messaging is required, and the URL reaches every neutron process and every migration Job through the OS_DEFAULT__TRANSPORT_URL environment override sourced from the derived transport-URL Secret, never through the rendered file.

Two options a reader may look for are rendered nowhere. Bridge mappings are a property of the node, so they live on the OVNChassis as spec.bridgeMappings and reach the local OVS as ovn-bridge-mappings. enable_distributed_floating_ip is absent from the defaults; the option catalog knows it, so spec.extraConfig can set it.

Four values never enter either file: the database password, the broker password, the service-user password, and, while spec.nova is set, the notifier password. Each arrives as an oslo.config environment override (OS_DATABASE__CONNECTION, OS_DEFAULT__TRANSPORT_URL, OS_KEYSTONE_AUTHTOKEN__PASSWORD, OS_NOVA__PASSWORD), which is why the [database] connection value in the rendered file is a placeholder URL: oslo.config parses the file before the override is applied, so the placeholder has to be syntactically valid.

Defaulting and validation ​

The mutating webhook applies the shared DeploymentSpec default to both spec.deployment and spec.workers.deployment (replicas 3) and writes no resources, which the reconciler resolves when it renders each Deployment. It materializes the cache backend dogpile.cache.pymemcache, materializes spec.logging and its baseline (text / INFO / debug: false) so no reconciler dereferences a nil pointer, and fills the ServiceUserSpec identity defaults (neutron / service / Default / Default, secretRef.key → password). Inside a present spec.nova it fills the notifier identity the same way (neutron-nova / service / Default / Default, secretRef.key → password); an absent block stays absent, because that is what keeps the port notifications off. It fills an empty spec.ovn.centralRef.namespace with the CR's own namespace. For the halves a CR carries it fills spec.messaging.secretRef.key with transport_url and spec.messaging.tls.caBundleSecretRef.key with ca.crt. When spec.apiServer is present it applies the shared uWSGI leaf defaults.

Two defaults are resolved at reconcile time and never written into the stored CR, so an unset field keeps tracking the operator default across upgrades: DefaultOVNDBSyncSchedule (0 * * * *) and DefaultOVNDBSyncMode (log).

metadata.name is bounded at 40 characters, MaxNeutronNameLength, computed as MaxCronJobNameLength (52) minus len("-ovn-db-sync"). The bound is enforced on create alone: metadata.name is immutable, so on update the rule could only fire against an object a pre-upgrade operator already admitted, and it would refuse every update to that CR with no field left to edit.

text
name must be at most %d characters: the ovn-db-sync CronJob appends %q and Kubernetes caps CronJob names at %d characters

The three arguments are the bound (40), the suffix (-ovn-db-sync), and MaxCronJobNameLength (52).

An update to a CR that is being deleted and leaves the spec unchanged is admitted without validation. That is the finalizer removal the reconciler issues, and an unchanged spec admitted earlier can fail today's rules: a PriorityClass deleted since, a topology-spread constraint that still names only the name and instance labels, or a worker block that predates its validation. Rejecting the removal would hold the CR in Terminating. An update that changes the spec of a deleting CR is still validated.

Schema-layer rules ​

These hold even when the webhook is down.

MessageWhere it comes from
targetClusterRef is immutableTwo CEL transition rules on NeutronSpec, one for adding or removing the ref and one for renaming it
exactly one of image.tag or image.digest must be setInherited from commonv1.ImageSpec on spec.image
exactly one of clusterRef or host must be setInherited from commonv1.DatabaseSpec
credentialsMode Dynamic requires clusterRef (managed mode)Inherited from commonv1.DatabaseSpec
exactly one of clusterRef or servers must be setInherited from commonv1.CacheSpec
clusterRef.name must not contain a newline or carriage returnInherited from commonv1.CacheSpec, alongside the ^[^\n\r]*$ items pattern on servers
exactly one of clusterRef or secretRef must be setInherited from commonv1.MessagingSpec
preStopSleepSeconds must be strictly less than terminationGracePeriodSecondsInherited from commonv1.DeploymentSpec on both deployment blocks; the rule substitutes the effective defaults 5 and 30 for an unset pointer
httpKeepAliveTimeout may only be set when httpKeepAlive is trueInherited from commonv1.UWSGISpec
at least one of targetCPUUtilization or targetMemoryUtilization must be setInherited from commonv1.AutoscalingSpec
minReplicas must not exceed maxReplicasInherited from commonv1.AutoscalingSpec
at least one ingress source must be specifiedInherited from commonv1.NetworkPolicySpec
logger name must not be emptyInherited from commonv1.LoggingSpec on perLoggerLevels
per-logger level must be one of DEBUG, INFO, WARNING, ERROR, CRITICALInherited from commonv1.LoggingSpec on perLoggerLevels

The Enum marker on spec.ovnDBSync.syncMode and the Pattern markers on spec.openStackRelease, spec.keystoneEndpoint and spec.keystonePublicEndpoint belong here too.

Webhook rules ​

The validating webhook accumulates every violation into one admission response. Most rules repeat a schema-layer check so the invariant survives an object that reached etcd through a bypass; the extraConfig rules and the cron grammar have no schema counterpart at all.

Messaging and the OVN reference:

MessageTrigger
exactly one of clusterRef or secretRef must be setspec.messaging names both modes or neither
caBundleSecretRef.name must be set when spec.messaging.tls is configuredA TLS block with no CA bundle name has nothing to verify the broker against
centralRef.name must be set (the OVNCentral this Neutron programs)spec.ovn.centralRef.name is empty. A Neutron without a control plane has nothing to program
serviceUser.secretRef.name must be set when spec.nova is configured: it carries the password Neutron notifies Nova withspec.nova is present with an empty serviceUser.secretRef.name. A notifier with no password to authenticate with has every event refused

Keystone endpoints and URL shapes, applied to keystoneEndpoint when non-empty and to keystonePublicEndpoint only when set:

MessageTrigger
keystoneEndpoint must be set (the Keystone auth_url the Neutron pods reach)The field is empty
must be a valid URL: %vurl.Parse refuses the value; the argument is the parser's own error
scheme must be http or httpsThe parsed scheme is anything else
URL must include a hostThe parsed URL carries no host

Control characters. Each of these values is written into the rendered INI as %s = %s, so a newline would inject a whole config line past the (section, key)-keyed ownership and catalog gates:

MessageApplies to
value must not contain a newline or carriage return: it is rendered verbatim into neutron.conf, so a newline injects arbitrary config linesspec.region, the four spec.serviceUser identity fields, spec.nova.region and the four spec.nova.serviceUser identity fields (read through a zero-valued NovaSpec when the block is absent, so the check costs nothing there), spec.ovn.centralRef.name, spec.ovn.centralRef.namespace, and spec.gateway.hostname
value must not contain a newline or carriage return: it is rendered verbatim into the service configuration file, so a newline injects arbitrary config linesspec.cache.clusterRef.name and each entry of spec.cache.servers, through the shared cache validator

spec.gateway.hostname is on the first list although the config renderer never writes it: in this operator the hostname reaches the generated HTTPRoute and nothing else. The guard therefore sits ahead of the Gateway API's own hostname validation, which runs a pipeline step after the config has been written and mounted. The two Keystone endpoints need no entry at all: url.Parse already refuses control bytes.

Image, replicas and graceful termination:

MessageTrigger
repository must be setspec.image.repository is empty
exactly one of image.tag or image.digest must be setBoth or neither are set
replicas must be at least 1spec.deployment.replicas below 1
terminationGracePeriodSeconds must be at least 10spec.deployment.terminationGracePeriodSeconds is set below 10
preStopSleepSeconds must be non-negativespec.deployment.preStopSleepSeconds is set below 0
preStopSleepSeconds (%d) must be strictly less than terminationGracePeriodSeconds (%d)The two resolved values leave no drain window. Nil pointers resolve to 5 and 30, so the rule holds when one or both are omitted

uWSGI and the rollout strategy:

MessageTrigger
harakiri (%d) must be strictly less than terminationGracePeriodSeconds - preStopSleepSeconds (%d)The worst-case per-request kill would fall outside the drain window. The arguments are the harakiri value and the resolved window
httpKeepAliveTimeout may only be set when httpKeepAlive is trueA timeout set beside an explicit httpKeepAlive: false. A nil pointer counts as unset and resolves to true, so only the explicit false conflicts
rollingUpdate must not be set when strategy.type is RecreateThe Deployment controller would reject the object at apply time

Autoscaling:

MessageTrigger
maxReplicas must be at least 1spec.autoscaling.maxReplicas below 1
minReplicas must be at least 1spec.autoscaling.minReplicas is set below 1
minReplicas must not exceed maxReplicasThe two bounds cross
maxReplicas must be >= spec.deployment.replicas (%d) when minReplicas is not set, because minReplicas defaults to spec.deployment.replicasAn unset minReplicas would resolve above maxReplicas and produce an HPA the API server rejects
targetCPUUtilization must be at least 1The target is below 1. There is no upper bound, as in autoscaling/v2
targetMemoryUtilization must be at least 1The target is below 1. There is no upper bound, as in autoscaling/v2
%s %d can never be reached: no container of the API pod can use more than %d%% of its %s request, because its %s limit caps it there; set a %s limit above the request, or a target of at most %dA target above 100 that the limits of spec.deployment.resources make unreachable, reported at the target. A block that names no memory renders the same figure as request and limit, so a memory target above 100 needs a memory limit above the request or a request without a limit
stabilizationWindowSeconds must be between 0 and 3600spec.autoscaling.behavior.<scaleUp|scaleDown>.stabilizationWindowSeconds is outside the autoscaling/v2 range
Unsupported value (selectPolicy, policy type)A selectPolicy other than Max, Min or Disabled, or a policy type other than Pods or Percent
value must be greater than zeroA behavior policy value of zero or below
periodSeconds must be between 1 and 1800A behavior policy periodSeconds outside the autoscaling/v2 range
tolerance must not be negativeA negative behavior tolerance
at least one of targetCPUUtilization or targetMemoryUtilization must be setAn autoscaling block with no target
%s request must be greater than zero while %s is set: the HorizontalPodAutoscaler divides the pods' usage by the sum of their containers' requestsA zero or negative request in spec.deployment.resources for the resource a set target measures, reported at requests.<cpu|memory>. The arguments are the resource and the target field
%s limit must be greater than zero while %s is set: without a request the API server copies the limit into the request, and the HorizontalPodAutoscaler divides the pods' usage by the sum of their containers' requestsspec.deployment.resources names no request for the resource a set target measures, and its limit is zero or negative, reported at limits.<cpu|memory>. The arguments are the resource and the target field

Network policy, gateway, resources and scheduling:

MessageTrigger
at least one ingress source must be specifiedspec.networkPolicy.ingress is empty. A policy with no source would allow every source
hostname must be set when spec.gateway is configuredspec.gateway.hostname is empty
parentRef.name must be set when spec.gateway is configuredspec.gateway.parentRef.name is empty
%s request must not exceed limit (%s)A request in spec.deployment.resources above its own limit. The arguments are the resource name and the limit
labelSelector is required on each TopologySpreadConstraintA constraint in spec.deployment.topologySpreadConstraints carries none
labelSelector.matchLabels must equal the Deployment selector labels %vThe selector does not equal the API Deployment's pod selector: the neutron name label, the instance label, and app.kubernetes.io/component: api. A selector without the component label also matches the worker and ovn-db-sync pods and is rejected
matchExpressions are not allowed; labelSelector must use matchLabels onlyA constraint selects with expressions
field.NotFound on spec.deployment.priorityClassNameThe named PriorityClass does not exist. The check is skipped when no lookup client is injected
failed to look up PriorityClass: %wThe lookup itself failed
field.Invalid on spec.deployment.nodeSelectorA key is not a qualified label name, or a value (reported at nodeSelector[<key>]) is not a valid label value
field.Invalid / field.NotSupported on spec.deployment.tolerations[i]A toleration the API server would refuse: an invalid key, an empty key without Exists, Exists with a value, an Lt or Gt value that is not an integer, tolerationSeconds without NoExecute, or an unknown operator or effect
%s request must not exceed limit (%s) on spec.jobs.resourcesA request in spec.jobs.resources above its own limit
field.NotFound on spec.jobs.priorityClassNameThe named PriorityClass does not exist. "" opts out of the fallback and is not looked up
field.Invalid on spec.jobs.nodeSelector / spec.jobs.tolerations[i]The node selector and toleration rules above, applied to spec.jobs

The worker block. spec.workers.deployment runs the pod-level rules of spec.deployment with the same messages, reported at spec.workers.deployment.*: the replica floor, the grace and preStop bounds and their drain window, the Recreate-vs-rollingUpdate check, the request within its limit, the PriorityClass lookup (field.NotFound), and the node selector and toleration rules. The harakiri rule stays with the API block, because uWSGI runs only in the API pods. The topology-spread rule differs:

MessageTrigger
topologySpreadConstraints is not supported here: the operator projects the periodic-workers and ovn-maintenance-worker Deployments from this block, each with its own selector, so a constraint set here would spread each worker's pods against the pods of the other components. An empty list, which only switches the injected defaults off, is acceptedfield.Forbidden on a non-empty spec.workers.deployment.topologySpreadConstraints. An empty or unset list is admitted

Secret store, target cluster and logging:

MessageTrigger
store name must be setspec.secretStoreRef is present with an empty name
field.NotSupported on spec.secretStoreRef.kindA kind outside ClusterSecretStore and SecretStore. An empty kind is accepted and defaults to ClusterSecretStore
target cluster name must be setspec.targetClusterRef is present with an empty name
targetClusterRef is immutable (adding or removing it after creation is not permitted)An update adds or drops the ref. Both strand the children already created on the previously selected cluster
targetClusterRef is immutable (the children already exist on the previously named cluster)An update renames the ref
field.NotSupported on spec.logging.formatA format outside text and json
field.NotSupported on spec.logging.levelA level outside DEBUG, INFO, WARNING, ERROR, CRITICAL
logger name must not be emptyA perLoggerLevels entry keyed on the empty string
logger name must not contain a newline or carriage return: it is rendered verbatim into neutron.conf, so a newline injects arbitrary config linesA logger name with a control character. The name renders into the [DEFAULT] default_log_levels CSV
level must be one of DEBUG, INFO, WARNING, ERROR, CRITICALA perLoggerLevels value outside the set

spec.ovnDBSync, whose cron grammar has no schema counterpart:

MessageTrigger
invalid cron expression: %vspec.ovnDBSync.schedule is non-empty and cron.ParseStandard refuses it. The argument is the parser's own error. An empty schedule is not parsed, since it resolves the operator default
field.NotSupported on spec.ovnDBSync.syncMode, listing log and repairA mode outside the enum, re-checked here for an object that bypassed the schema

The release floor, which has no schema counterpart. The CRD pattern admits any release of the YYYY.N cadence, and the floor moves with the operator version:

MessageTrigger
must be 2026.1 or later: this operator version no longer supports OpenStack releases below 2026.1spec.openStackRelease names a release below 2026.1, the oldest this operator version supports, on create or when an update changes it. A change from one release below the floor to another is rejected too

An update that keeps a stored value below the floor is admitted with a warning, so an unrelated edit never blocks the resource:

text
spec.openStackRelease %q 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.openStackRelease to 2026.1 or later.

spec.extraConfig is a preserve-unknown-fields map CEL cannot constrain, so every guard on it is the webhook's alone:

MessageTrigger
extraConfig section name must not be emptyA section keyed on the empty string, which would render a nameless [] header
extraConfig section name must not contain a newline or carriage returnA control character in a section name
extraConfig key must not be emptyAn option keyed on the empty string, which would render a bare = value line
extraConfig key and value must not contain a newline or carriage return: the rendered INI writes them verbatim, so a newline injects arbitrary config linesA control character in an option key or value
%s is managed via %s and must not be set in extraConfig (%s)An override of a Rejected owned key. The arguments are the key, the field or computation that owns it, and its impact; the parenthesis is dropped for an entry with no impact text
no such option in the neutron %s option catalogAn option name the release catalog does not know. The argument is the catalog's release
no such section in the neutron %s option catalogA section name the release catalog does not know

The catalog is the flat union of neutron.conf, ml2_conf.ini and neutron_ovn_metadata_agent.ini, with no per-file provenance, so a section belonging to the metadata agent passes here and oslo.config ignores it at runtime. Owned keys are exempt from the scan, so an operator-owned key never doubles as an unknown option. The check re-runs on update only when spec.extraConfig or spec.openStackRelease changed, so an unrelated edit such as a replica change cannot retroactively reject a CR whose extraConfig a regenerated catalog has since invalidated.

Four admission warnings come out of the same check:

text
spec.extraConfig was not validated against an option catalog: spec.openStackRelease does not name an OpenStack release
spec.extraConfig was not validated against an option catalog: no catalog for release %q is embedded in this operator build
spec.extraConfig [%s] %s: deprecated option in neutron %s, replaced by %s
spec.extraConfig [%s] %s: deprecated option in neutron %s with no replacement

The first two are the fail-open paths: an unresolvable release skips the option check with one warning rather than blocking admission. The last two name a deprecated-but-accepted option and, where upstream declares one, its replacement.

Rejected owned keys ​

config_ownership.go records every key the operator computes and renders, one registry per kind. An entry is honored-and-reported unless honoring the override would already have done the damage by the time ExtraConfigHealthy could surface it, which is the case for a credential the rendering copies into the ConfigMap every pod mounts, a path or connection string that points a process somewhere the operator did not provision, and a switch that selects a security control. Those fifteen are refused at admission for the Neutron kind:

KeyOwned byWhy the override is refused
[DEFAULT] auth_strategyoperator-computedIt names the WSGI pipeline api-paste.ini serves the API through, and keystone is the only one that runs keystonemiddleware. Anything else serves every request unauthenticated, reachable from outside the cluster while spec.gateway is set
[DEFAULT] api_paste_configoperator-computedIt names the pipeline definition. A path the pod does not carry fails the API on start; one it does carry can drop the auth filter
[DEFAULT] transport_urlspec.messagingThe runtime value arrives through OS_DEFAULT__TRANSPORT_URL, so a file value is inert and only copies the broker credentials into the rendered ConfigMap
[database] connectionspec.databaseThe runtime value arrives through OS_DATABASE__CONNECTION, so a file value is inert and only copies the database password into the rendered ConfigMap
[keystone_authtoken] passwordspec.serviceUser.secretRefThe middleware reads the password from OS_KEYSTONE_AUTHTOKEN__PASSWORD, so a file value is inert and only copies the service password into the rendered ConfigMap
[nova] passwordspec.nova.serviceUser.secretRefThe notifier reads the password from OS_NOVA__PASSWORD, so a file value is inert and only copies the notifier password into the rendered ConfigMap
[securitygroup] enable_security_groupoperator-computedIt is what makes the ML2/OVN mechanism driver program the ACLs a port's security groups describe. Disabling it leaves every instance port reachable from every other
[ovn] ovn_nb_connectionspec.ovn.centralRefThe connection string is resolved from the referenced OVNCentral; another address points the mechanism driver at a logical model it does not own
[ovn] ovn_sb_connectionspec.ovn.centralRefThe Southbound half of the same rule
[ovn] ovn_nb_private_keyoperator-computedThe operator mounts the client keypair the OVNCentral issuer signed; another path names a file the pod does not carry, and the connection falls back to no client identity
[ovn] ovn_nb_certificateoperator-computedThe certificate half of the same keypair
[ovn] ovn_nb_ca_certoperator-computedThe CA bundle verifies the database endpoint; another path either fails the handshake or trusts a server the operator did not provision
[ovn] ovn_sb_private_keyoperator-computedThe Southbound keypair, on the same terms as the Northbound one
[ovn] ovn_sb_certificateoperator-computedThe Southbound certificate
[ovn] ovn_sb_ca_certoperator-computedThe Southbound CA bundle

An update that carries a refused key over from the stored object with the same value is admitted with a warning instead of refused. [nova] password joined the list when the operator started owning the notifier account, and before that spec.extraConfig was the documented way to configure the notifier, so refusing it on every update, the finalizer removal on delete included, would leave such a CR with no update to remove it through. A new or changed value is still refused.

Every other entry in the registry is honored and reported: the remaining [DEFAULT] keys, including the two notify_nova_on_port_* switches spec.nova owns; the [keystone_authtoken] keys keystoneauth.Section renders and the eight [nova] keys it renders beside them, owned by spec.nova, spec.keystoneEndpoint and the operator; the [oslo_messaging_notifications], [oslo_messaging_rabbit] and [oslo_concurrency] keys; the [ml2] and per-type-driver keys; and the two remaining [ovn] keys ovn_l3_scheduler and ovn_metadata_enabled. A conditionally rendered key is registered unconditionally: the registry records that a key is not the user's to set, not that it is currently rendered.

Status ​

FieldTypeDescription
conditions[]metav1.ConditionList-map keyed by type; see Conditions
observedGenerationint64The .metadata.generation the controller last reconciled
endpointstringThe Neutron API URL, http://{name}.{namespace}.svc.cluster.local:9696. It is the cluster-local Service URL whether or not spec.gateway is set, and it is stamped once the API Deployment is available
installedReleasestringThe OpenStack release whose schema is currently installed, promoted to spec.openStackRelease after a successful db-sync
installedImagestringThe spec.image reference that migrated the schema installedRelease names. A digest carries no parseable release, so without this field a spec.openStackRelease bump that left spec.image untouched would run no migration and still promote installedRelease; the release gate compares against it to refuse that transition
targetReleasestringThe spec.openStackRelease an active upgrade is converging to. Empty in steady state
upgradePhasecommonv1.UpgradePhaseThe current expand-migrate-contract phase (Expanding, Migrating, RollingUpdate, Contracting); empty when no upgrade is in flight

Neutron's alembic tree splits into an expand and a contract branch with no data-migration command in between. The shared flow runs a Job for every phase, so the Migrating Job runs neutron-db-manage current: it prints the revision each branch sits at and exits 0, which keeps the phase readable in the Job log.

Conditions ​

Fifteen sub-reconcilers report under eleven condition types: four of them share SecretsReady and two share OVNEndpointsReady. The aggregate Ready is True only when all eleven are. For the order they run in and what each one does, see the reconciler reference.

TypeStatusReasonMeaning
SecretsReadyTrueSecretsAvailableThe selected store is ready and both credential Secrets carry their keys
SecretsReadyFalseSecretStoreNotReadyThe External Secrets store this Neutron selected is not ready. Checked ahead of the individual Secrets, so an upstream outage surfaces even while per-ExternalSecret caches still report their last successful sync
SecretsReadyFalseClusterSecretStoreUnsupportedThe operator runs with --namespace and this Neutron's effective store is a ClusterSecretStore, which an omitted spec.secretStoreRef resolves to. A namespaced Role cannot grant the cluster-scoped kind, so the operator does not read the store; select a SecretStore in the Neutron's namespace
SecretsReadyFalseWaitingForDBCredentialsThe database credentials Secret is missing or carries no username/password. Also set by the step that derives the connection Secret
SecretsReadyFalseWaitingForServiceUserCredentialsThe service-user Secret is missing or carries no password under the configured key
SecretsReadyFalseWaitingForMessagingCredentialsThe managed RabbitmqCluster has published no default-user Secret yet, or the brownfield Secret carries no transport URL
SecretsReadyFalseConfigErrorRendering or pruning the config ConfigMap failed. Config artefacts gate the same downstream graph as the credential Secrets, so they share that condition and there is no separate one for them
SecretsReadyFalseTargetClusterUnavailablespec.targetClusterRef names a cluster that does not resolve. Reported on the pipeline's first condition, and on the same condition while a deleting CR waits for its target cluster to come back
OVNEndpointsReadyTrueOVNEndpointsResolvedBoth database addresses are resolved and the message names them
OVNEndpointsReadyFalseOVNCentralNotFoundThe OVNCentral named by spec.ovn.centralRef does not exist. A Neutron applied before its central is an ordinary ordering of two objects in one manifest, so this polls
OVNEndpointsReadyFalseOVNCentralReadErrorReading the central failed
OVNEndpointsReadyFalseOVNEndpointsPendingThe central publishes no Northbound or Southbound address yet, or no client Secret name. Across a cluster boundary the message names the two externallyReachable fields to set, since only the node ports are routable there
OVNEndpointsReadyFalseOVNClientSecretPendingThe central published a client Secret name before cert-manager wrote the Secret
OVNEndpointsReadyFalseOVNClientSecretIncompleteThe source Secret carries no tls.crt, tls.key or ca.crt yet; the message names the missing key
OVNEndpointsReadyFalseOVNClientSecretReadErrorReading the source Secret failed
OVNEndpointsReadyFalseOVNClientSecretMirrorFailedWriting the mirrored {name}-ovn-client Secret failed
OVNEndpointsReadyFalseTargetClusterUnavailableThe central's own target cluster does not resolve, so its client Secret cannot be read
DatabaseReadyTrueDatabaseSyncedThe schema is at the head revision for the installed release
DatabaseReadyFalseClusterNotReadyThe referenced MariaDB cluster is not ready
DatabaseReadyFalseWaitingForDatabaseThe MariaDB Database, User or Grant CR is not ready
DatabaseReadyFalseWaitingForConfigNo rendered config exists yet, which means the OVN endpoints are still unresolved. The migration Jobs mount that ConfigMap as their whole /etc/neutron, so an empty name would render a volume the API server rejects
DatabaseReadyFalseDBSyncInProgressThe db-sync Job is running
DatabaseReadyFalseDBSyncFailedThe db-sync Job failed
DatabaseReadyFalseVersionParseErrorThe installed or the requested release does not parse
DatabaseReadyFalseDowngradeNotSupportedThe requested release is older than the installed one
DatabaseReadyFalseUpgradePathInvalidThe jump skips a release; upgrade one release at a time
DatabaseReadyFalseImageReleaseMismatchA tag-pinned spec.image names a different release than spec.openStackRelease, or a release bump left spec.image untouched so no migration would run. The message names both readings of the second case, because the recovery differs
DatabaseReadyFalseUpgradeTargetChangedspec.openStackRelease changed while an upgrade was in flight
DatabaseReadyFalseExpandInProgress, MigrateInProgress, ContractInProgressThe phase Job of that name is running
DatabaseReadyFalseExpandFailed, MigrateFailed, ContractFailedThe phase Job of that name failed
DatabaseReadyFalseUpgradeRollingUpdateThe upgrade waits for the Deployment to roll onto the target-release image before contracting
OVNDBSyncReadyTrueOVNDBSyncNotRequiredspec.ovnDBSync is unset, so no CronJob exists. The absence is the configured state
OVNDBSyncReadyTrueOVNDBSyncScheduledThe CronJob is in place and the most recent terminal run did not fail. The message names the resolved schedule and mode
OVNDBSyncReadyTrueOVNDBSyncSuspendedspec.ovnDBSync.suspend is set. It outranks a failed run, because a suspended CronJob spawns no successor to supersede it; the failure is still named in the message
OVNDBSyncReadyFalseOVNDBSyncJobFailedThe newest terminal run failed, so the two databases are no longer being compared
DeploymentReadyTrueDeploymentReadyThe API Deployment is available
DeploymentReadyFalseWaitingForDeploymentThe Deployment is rolling out, or the upgrade's rolling-update phase waits for every replica to be updated, ready and counted before the contract Jobs drop what the old pods still read
WorkersReadyTrueWorkersReadyBoth worker Deployments are available
WorkersReadyFalseWaitingForWorkersAt least one of them is not
NeutronAPIReadyTrueAPIHealthyAn HTTP GET against the cluster-local API root answered 2xx. / is the version document, which the API pods answer without a token and without touching the database
NeutronAPIReadyFalseAPIUnhealthyThe probe answered a non-2xx status
NeutronAPIReadyFalseEndpointNotReadystatus.endpoint is not stamped yet, or the endpoint does not resolve
NeutronAPIReadyFalseHealthCheckTimeoutThe probe exceeded its deadline
NeutronAPIReadyFalseConnectionFailedThe connection was refused
NeutronAPIReadyFalseHealthCheckFailedThe probe failed for another reason; the message carries it
HTTPRouteReadyTrueHTTPRouteAcceptedThe Gateway controller reports Accepted=True on the route's parent
HTTPRouteReadyTrueHTTPRouteNotRequiredspec.gateway is unset, so any previous route was deleted
HTTPRouteReadyFalseHTTPRouteNotAcceptedThe parent Gateway has not accepted the route
HTTPRouteReadyFalseGatewayAPINotInstalledThe cluster the children land on does not serve the HTTPRoute kind while spec.gateway is set
HTTPRouteReadyFalseCapabilityProbeFailedThe probe for that kind failed against the target cluster's RESTMapper
HPAReadyTrueHPAReadyThe HorizontalPodAutoscaler matches the desired state
HPAReadyTrueHPANotRequiredspec.autoscaling is unset, so any previous HPA was deleted
NetworkPolicyReadyTrueNetworkPolicyReadyThe NetworkPolicy matches the desired state
NetworkPolicyReadyTrueNetworkPolicyNotRequiredspec.networkPolicy is unset, so any previous policy was deleted and traffic flows unrestricted
ReadyTrueAllReadyAll eleven sub-conditions are True
ReadyFalseNotAllReadyAt least one is not
VPAReadyTrueVPAReadyThe VerticalPodAutoscaler of every opted-in workload is applied; the message names them. See VerticalAutoscalingSpec
VPAReadyTrueVPANotRequiredNo workload opts in (every verticalAutoscaling block unset); a VPA the CR created before is deleted
VPAReadyFalseVPANotInstalledA workload opts in, but the cluster the children land on does not serve autoscaling.k8s.io/v1 VerticalPodAutoscaler. Nothing is created, and the other conditions still converge
VPAReadyFalseCapabilityProbeFailedThe target cluster the CR names could not be probed for the kind
VPAReadyFalseVPAErrorListing, applying or deleting a VPA failed; the message carries the error

ExtraConfigHealthy is set beside these on every pass that renders a config, reporting the honored overrides of operator-owned keys. It is informational and stays out of the Ready aggregation.

NeutronAPIReady, HTTPRouteReady, HPAReady and NetworkPolicyReady are the members of the post-deployment parallel group. Each of them always resolves, configured-ready or not-required, so a cluster without a gateway, an HPA or a network policy still reaches Ready.

Sub-Resource Naming Convention ​

The API Deployment, its Service, the PodDisruptionBudget, the HorizontalPodAutoscaler, the NetworkPolicy, the HTTPRoute and the MariaDB Database/User/Grant take the bare CR name with no suffix, matching the keystone convention. A Neutron named neutron in the openstack namespace is therefore reachable in-cluster at neutron.openstack.svc.cluster.local:9696, since the Service DNS name is the CR name and 9696 is the port the uWSGI-served API listens on.

The derived and content-addressed children carry a suffix:

ResourceNameNotes
Config ConfigMap{name}-config-<hash>Immutable, content-hashed neutron.conf and ml2_conf.ini, plus uwsgi.ini and, for json logging, logging.conf; 3 historical retained
DB-connection Secret{name}-db-connectionThe derived pymysql DSN, stable name, consumed through OS_DATABASE__CONNECTION
Transport-URL Secret{name}-transport-urlThe derived rabbit:// URL, consumed through OS_DEFAULT__TRANSPORT_URL
OVN client Secret{name}-ovn-clientThe mirror of the client identity the OVNCentral publishes, carrying tls.crt, tls.key and ca.crt and nothing else. The source lives in the central's namespace, on a placed Neutron even on another cluster, so the pods mount the copy
DB-sync Job{name}-db-syncneutron-db-manage upgrade head. There is no schema-check Job: the alembic upgrade is idempotent, so a second read-only run would assert nothing
Expand Job{name}-db-expandneutron-db-manage upgrade --expand, release upgrades only
Migrate Job{name}-db-migrateneutron-db-manage current, release upgrades only
Contract Job{name}-db-contractneutron-db-manage upgrade --contract, release upgrades only
OVN sync CronJob{name}-ovn-db-syncneutron-ovn-db-sync-util, only while spec.ovnDBSync is set. The suffix is what bounds metadata.name to 40 characters
Worker Deployment{name}-periodic-workersneutron-periodic-workers, the recurring maintenance tasks of the ML2 plugin
Worker Deployment{name}-ovn-maintenance-workerneutron-ovn-maintenance-worker, which reconciles the Northbound model against the Neutron database

One Neutron owns three Deployments, so each of them selects on its own component label. A selector without the component key would have the three count and adopt each other's pods, and the Service would route API traffic to a worker that serves no HTTP. The PodDisruptionBudget selects the API component and excludes Job pods, which carry no readiness probe and would otherwise be Ready from their first moment and raise disruptionsAllowed.

ovnDBSync ​

A nil spec.ovnDBSync means no CronJob. Any CronJob a previous spec created is deleted and OVNDBSyncReady reports OVNDBSyncNotRequired: the sync reads, and in repair mode rewrites, the entire logical model, so scheduling it is a choice the CR has to make.

With the block present, schedule and syncMode are resolved at reconcile time when empty, to 0 * * * * and log. Resolving them at reconcile time keeps an unset field tracking the operator default across upgrades; a defaulting webhook would freeze today's value into the stored CR.

The two modes differ in what they leave behind. neutron-ovn-db-sync-util --ovn-neutron_sync_mode log reports the drift and exits 0 whether or not the two databases agree, so a run that found the Northbound out of step is indistinguishable from a clean one here: the condition reports Job outcomes, and the report lives in the run's log. repair deletes every Northbound object the Neutron database cannot account for and creates the ones it is missing, so switch to it for a run you intend and switch back afterwards, or use suspend for the window in which someone is editing the Northbound database by hand.

Each firing gets one Job with concurrencyPolicy: Forbid, no retries, and a one-hour active deadline. Two runs would walk the same two databases and, in repair mode, write the second one, so the later run would undo what the earlier is halfway through. A retry would walk the same unreachable Northbound and produce the same failure minutes later; the next firing is the retry. The deadline is what keeps a wedged run observable, since a pod stuck in ImagePullBackOff or a utility blocked on an unresponsive Northbound reaches no terminal state on its own.

When a run fails, the condition message and the Warning event (see Controller events) both name the line to look for in the pod log:

text
Could not retrieve schema from <address>

It means the Northbound at that address was unreachable, and not that the two databases disagree.

For the procedure around a repair-mode run, see Repair OVN drift with db-sync.

Example ​

yaml
apiVersion: neutron.openstack.c5c3.io/v1alpha1
kind: Neutron
metadata:
  name: neutron-basic
  namespace: openstack
spec:
  openStackRelease: "2026.1"
  deployment:
    replicas: 2
  workers:
    deployment:
      replicas: 1
  image:
    repository: ghcr.io/c5c3/neutron
    tag: "2026.1"
  database:
    clusterRef:
      name: openstack-db
    database: neutron_basic
    secretRef:
      name: neutron-db
  cache:
    clusterRef:
      name: openstack-memcached
  messaging:
    secretRef:
      name: neutron-basic-messaging
  ovn:
    centralRef:
      name: neutron-basic-ovn
  keystoneEndpoint: http://keystone.openstack.svc.cluster.local:5000/v3
  serviceUser:
    username: admin
    projectName: admin
    userDomainName: Default
    projectDomainName: Default
    secretRef:
      name: keystone-admin
      key: password

This is the fixture the basic-deployment suite applies (tests/e2e/neutron/basic-deployment/02-neutron-cr.yaml). Its keystoneEndpoint is the cluster-local convention URL every colocated service uses, and the leg deploys no Keystone: nothing resolves that address there, so spec.serviceUser names the pre-seeded keystone-admin Secret and the suite asserts on the operator's own conditions, with no authenticated API call in it. The API runs two replicas because the PodDisruptionBudget builder renders minAvailable: 1 only above one replica, which is what makes the suite's PDB assertion meaningful; both workers stay at one, since neither serves a request.