diff --git a/README.md b/README.md index ad87d60f85..eec6b96e45 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,18 @@ OVNKubernetes supports the following configuration options, all of which are opt * `egressIPConfig`: holds the configuration for EgressIP options. * `reachabilityTotalTimeoutSeconds`: Set EgressIP node reachability total timeout in seconds, 0 means disable reachability check and the default is 1 second. +#### DPU Host Mode Support + +OVN-Kubernetes supports specialized hardware deployments such as DPU (Data Processing Unit) hosts through the `OVN_NODE_MODE` environment variable. In `dpu-host` mode, certain features are automatically disabled on those nodes regardless of cluster-wide configuration: + +- Egress IP and related features (egress firewall, egress QoS, egress service) +- Multicast support +- Multi-external gateway support +- Multi-network policies and admin network policies +- Network segmentation features + +This per-node feature enforcement is implemented through conditional logic in the startup scripts, allowing the same cluster configuration to work across heterogeneous node types. For detailed information about node modes and the technical implementation, see `docs/ovn_node_mode.md`. + These configuration flags are only in the Operator configuration object. Example from the `manifests/cluster-network-03-config.yml` file: diff --git a/bindata/network/ovn-kubernetes/common/008-script-lib.yaml b/bindata/network/ovn-kubernetes/common/008-script-lib.yaml index 1885ba99a2..264f1f25df 100644 --- a/bindata/network/ovn-kubernetes/common/008-script-lib.yaml +++ b/bindata/network/ovn-kubernetes/common/008-script-lib.yaml @@ -515,10 +515,41 @@ data: echo "I$(date "+%m%d %H:%M:%S.%N") - starting ovnkube-node" + # enable egress ip, egress firewall, egress qos, egress service + egress_features_enable_flag="--enable-egress-ip=true --enable-egress-firewall=true --enable-egress-qos=true --enable-egress-service=true" + init_ovnkube_controller="--init-ovnkube-controller ${K8S_NODE}" + multi_external_gateway_enable_flag="--enable-multi-external-gateway=true" + gateway_interface=br-ex + + # enable multicast + enable_multicast_flag="--enable-multicast" + + # Use OVN_NODE_MODE environment variable, default to "full" if not set + OVN_NODE_MODE=${OVN_NODE_MODE:-full} + # We check only dpu-host mode and not smart-nic mode here as currently we do not support it yet + # Once we support it, we will need to check for it here and add relevant code. + if [ "${OVN_NODE_MODE}" == "dpu-host" ]; then + # this is required for the dpu-host mode to configure right gateway interface + # https://github.com/ovn-kubernetes/ovn-kubernetes/pull/5327/files + gateway_interface="derive-from-mgmt-port" + ovnkube_node_mode="--ovnkube-node-mode dpu-host" + # disable egress ip for dpu-host mode as it is not supported + egress_features_enable_flag="" + + # disable multicast for dpu-host mode as it is not supported + enable_multicast_flag="" + + # disable init-ovnkube-controller for dpu-host mode as it is not supported + init_ovnkube_controller="" + + # disable multi-external-gateway for dpu-host mode as it is not supported + multi_external_gateway_enable_flag="" + fi + if [ "{{.OVN_GATEWAY_MODE}}" == "shared" ]; then - gateway_mode_flags="--gateway-mode shared --gateway-interface br-ex" + gateway_mode_flags="--gateway-mode shared --gateway-interface ${gateway_interface}" elif [ "{{.OVN_GATEWAY_MODE}}" == "local" ]; then - gateway_mode_flags="--gateway-mode local --gateway-interface br-ex" + gateway_mode_flags="--gateway-mode local --gateway-interface ${gateway_interface}" else echo "Invalid OVN_GATEWAY_MODE: \"{{.OVN_GATEWAY_MODE}}\". Must be \"local\" or \"shared\"." exit 1 @@ -558,12 +589,12 @@ data: fi multi_network_enabled_flag= - if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" == "true" && "${OVN_NODE_MODE}" != "dpu-host" ]]; then multi_network_enabled_flag="--enable-multi-network" fi network_segmentation_enabled_flag= - if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" && "${OVN_NODE_MODE}" != "dpu-host" ]]; then multi_network_enabled_flag="--enable-multi-network" network_segmentation_enabled_flag="--enable-network-segmentation" fi @@ -584,12 +615,12 @@ data: fi multi_network_policy_enabled_flag= - if [[ "{{.OVN_MULTI_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_POLICY_ENABLE}}" == "true"&& "${OVN_NODE_MODE}" != "dpu-host" ]]; then multi_network_policy_enabled_flag="--enable-multi-networkpolicy" fi admin_network_policy_enabled_flag= - if [[ "{{.OVN_ADMIN_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_ADMIN_NETWORK_POLICY_ENABLE}}" == "true" && "${OVN_NODE_MODE}" != "dpu-host" ]]; then admin_network_policy_enabled_flag="--enable-admin-network-policy" fi @@ -654,7 +685,7 @@ data: fi exec /usr/bin/ovnkube \ - --init-ovnkube-controller "${K8S_NODE}" \ + ${init_ovnkube_controller} \ --init-node "${K8S_NODE}" \ --config-file=/run/ovnkube-config/ovnkube.conf \ --ovn-empty-lb-events \ @@ -662,9 +693,7 @@ data: --inactivity-probe="${OVN_CONTROLLER_INACTIVITY_PROBE}" \ ${gateway_mode_flags} \ ${node_mgmt_port_netdev_flags} \ -{{- if eq .OVN_NODE_MODE "dpu-host" }} - --ovnkube-node-mode dpu-host \ -{{- end }} + ${ovnkube_node_mode} \ --metrics-bind-address "127.0.0.1:${metrics_port}" \ --ovn-metrics-bind-address "127.0.0.1:${ovn_metrics_port}" \ --metrics-enable-pprof \ @@ -680,7 +709,7 @@ data: ${admin_network_policy_enabled_flag} \ ${dns_name_resolver_enabled_flag} \ ${network_observability_enabled_flag} \ - --enable-multicast \ + ${enable_multicast_flag} \ --zone ${K8S_NODE} \ --enable-interconnect \ --acl-logging-rate-limit "{{.OVNPolicyAuditRateLimit}}" \ @@ -693,5 +722,7 @@ data: ${ovn_v4_masquerade_subnet_opt} \ ${ovn_v6_masquerade_subnet_opt} \ ${ovn_v4_transit_switch_subnet_opt} \ - ${ovn_v6_transit_switch_subnet_opt} + ${ovn_v6_transit_switch_subnet_opt} \ + ${egress_features_enable_flag} \ + ${multi_external_gateway_enable_flag} } diff --git a/bindata/network/ovn-kubernetes/managed/004-config.yaml b/bindata/network/ovn-kubernetes/managed/004-config.yaml index 44b0011f0f..ae43238950 100644 --- a/bindata/network/ovn-kubernetes/managed/004-config.yaml +++ b/bindata/network/ovn-kubernetes/managed/004-config.yaml @@ -33,16 +33,9 @@ data: dns-service-name="dns-default" [ovnkubernetesfeature] - enable-egress-ip=true - enable-egress-firewall=true - enable-egress-qos=true - enable-egress-service=true {{- if .ReachabilityNodePort }} egressip-node-healthcheck-port={{.ReachabilityNodePort}} {{- end }} -{{- if .OVN_MULTI_NETWORK_ENABLE }} - enable-multi-network=true -{{- end }} {{- if .OVN_NETWORK_SEGMENTATION_ENABLE }} {{- if not .OVN_MULTI_NETWORK_ENABLE }} enable-multi-network=true @@ -52,13 +45,6 @@ data: {{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} enable-preconfigured-udn-addresses=true {{- end }} -{{- if .OVN_MULTI_NETWORK_POLICY_ENABLE }} - enable-multi-networkpolicy=true -{{- end }} -{{- if .OVN_ADMIN_NETWORK_POLICY_ENABLE }} - enable-admin-network-policy=true -{{- end }} - enable-multi-external-gateway=true {{- if .DNS_NAME_RESOLVER_ENABLE }} enable-dns-name-resolver=true {{- end }} @@ -147,13 +133,6 @@ data: enable-preconfigured-udn-addresses=true {{- end }} {{- end }} -{{- if .OVN_MULTI_NETWORK_POLICY_ENABLE }} - enable-multi-networkpolicy=true -{{- end }} -{{- if .OVN_ADMIN_NETWORK_POLICY_ENABLE }} - enable-admin-network-policy=true -{{- end }} - enable-multi-external-gateway=true {{- if .DNS_NAME_RESOLVER_ENABLE }} enable-dns-name-resolver=true {{- end }} diff --git a/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml b/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml index a9173210fb..859aa3ef0b 100644 --- a/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml +++ b/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml @@ -184,8 +184,13 @@ spec: # will rollout control plane pods as well network_segmentation_enabled_flag= multi_network_enabled_flag= - if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" == "true" ]]; then multi_network_enabled_flag="--enable-multi-network" + fi + if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" != "true" ]]; then + multi_network_enabled_flag="--enable-multi-network" + fi network_segmentation_enabled_flag="--enable-network-segmentation" fi @@ -199,6 +204,18 @@ spec: preconfigured_udn_addresses_enable_flag="--enable-preconfigured-udn-addresses" fi + # Enable multi-network policy if configured (control-plane always full mode) + multi_network_policy_enabled_flag= + if [[ "{{.OVN_MULTI_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + multi_network_policy_enabled_flag="--enable-multi-networkpolicy" + fi + + # Enable admin network policy if configured (control-plane always full mode) + admin_network_policy_enabled_flag= + if [[ "{{.OVN_ADMIN_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + admin_network_policy_enabled_flag="--enable-admin-network-policy" + fi + echo "I$(date "+%m%d %H:%M:%S.%N") - ovnkube-control-plane - start ovnkube --init-cluster-manager ${K8S_NODE}" exec /usr/bin/ovnkube \ --enable-interconnect \ @@ -220,7 +237,15 @@ spec: ${multi_network_enabled_flag} \ ${network_segmentation_enabled_flag} \ ${route_advertisements_enable_flag} \ - ${preconfigured_udn_addresses_enable_flag} + ${preconfigured_udn_addresses_enable_flag} \ + --enable-egress-ip=true \ + --enable-egress-firewall=true \ + --enable-egress-qos=true \ + --enable-egress-service=true \ + --enable-multicast \ + --enable-multi-external-gateway=true \ + ${multi_network_policy_enabled_flag} \ + ${admin_network_policy_enabled_flag} volumeMounts: - mountPath: /run/ovnkube-config/ name: ovnkube-config diff --git a/bindata/network/ovn-kubernetes/managed/ovnkube-node.yaml b/bindata/network/ovn-kubernetes/managed/ovnkube-node.yaml index ff47818215..68caa980d2 100644 --- a/bindata/network/ovn-kubernetes/managed/ovnkube-node.yaml +++ b/bindata/network/ovn-kubernetes/managed/ovnkube-node.yaml @@ -394,6 +394,8 @@ spec: value: "{{.OVN_CONTROLLER_INACTIVITY_PROBE}}" - name: OVN_KUBE_LOG_LEVEL value: "4" + - name: OVN_NODE_MODE + value: "{{.OVN_NODE_MODE}}" {{ if .NetFlowCollectors }} - name: NETFLOW_COLLECTORS value: "{{.NetFlowCollectors}}" diff --git a/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml b/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml index 2e9ce2245d..717c3e6877 100644 --- a/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml +++ b/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml @@ -36,19 +36,13 @@ data: dns-service-name="dns-default" [ovnkubernetesfeature] - enable-egress-ip=true - enable-egress-firewall=true - enable-egress-qos=true - enable-egress-service=true + {{- if .ReachabilityTotalTimeoutSeconds }} egressip-reachability-total-timeout={{.ReachabilityTotalTimeoutSeconds}} {{- end }} {{- if .ReachabilityNodePort }} egressip-node-healthcheck-port={{.ReachabilityNodePort}} {{- end }} -{{- if .OVN_MULTI_NETWORK_ENABLE }} - enable-multi-network=true -{{- end }} {{- if .OVN_NETWORK_SEGMENTATION_ENABLE }} {{- if not .OVN_MULTI_NETWORK_ENABLE }} enable-multi-network=true @@ -61,10 +55,6 @@ data: {{- if .OVN_MULTI_NETWORK_POLICY_ENABLE }} enable-multi-networkpolicy=true {{- end }} -{{- if .OVN_ADMIN_NETWORK_POLICY_ENABLE }} - enable-admin-network-policy=true -{{- end }} - enable-multi-external-gateway=true {{- if .DNS_NAME_RESOLVER_ENABLE }} enable-dns-name-resolver=true {{- end }} diff --git a/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml b/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml index b08284ba65..c49b3c0574 100644 --- a/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml +++ b/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml @@ -135,8 +135,13 @@ spec: # will rollout control plane pods as well network_segmentation_enabled_flag= multi_network_enabled_flag= - if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" == "true" ]]; then multi_network_enabled_flag="--enable-multi-network" + fi + if [[ "{{.OVN_NETWORK_SEGMENTATION_ENABLE}}" == "true" ]]; then + if [[ "{{.OVN_MULTI_NETWORK_ENABLE}}" != "true" ]]; then + multi_network_enabled_flag="--enable-multi-network" + fi network_segmentation_enabled_flag="--enable-network-segmentation" fi @@ -149,6 +154,18 @@ spec: if [[ "{{.OVN_PRE_CONF_UDN_ADDR_ENABLE}}" == "true" ]]; then preconfigured_udn_addresses_enable_flag="--enable-preconfigured-udn-addresses" fi + + # Enable multi-network policy if configured (control-plane always full mode) + multi_network_policy_enabled_flag= + if [[ "{{.OVN_MULTI_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + multi_network_policy_enabled_flag="--enable-multi-networkpolicy" + fi + + # Enable admin network policy if configured (control-plane always full mode) + admin_network_policy_enabled_flag= + if [[ "{{.OVN_ADMIN_NETWORK_POLICY_ENABLE}}" == "true" ]]; then + admin_network_policy_enabled_flag="--enable-admin-network-policy" + fi if [ "{{.OVN_GATEWAY_MODE}}" == "shared" ]; then gateway_mode_flags="--gateway-mode shared" @@ -178,7 +195,15 @@ spec: ${network_segmentation_enabled_flag} \ ${gateway_mode_flags} \ ${route_advertisements_enable_flag} \ - ${preconfigured_udn_addresses_enable_flag} + ${preconfigured_udn_addresses_enable_flag} \ + --enable-egress-ip=true \ + --enable-egress-firewall=true \ + --enable-egress-qos=true \ + --enable-egress-service=true \ + --enable-multicast \ + --enable-multi-external-gateway=true \ + ${multi_network_policy_enabled_flag} \ + ${admin_network_policy_enabled_flag} volumeMounts: - mountPath: /run/ovnkube-config/ name: ovnkube-config diff --git a/bindata/network/ovn-kubernetes/self-hosted/ovnkube-node.yaml b/bindata/network/ovn-kubernetes/self-hosted/ovnkube-node.yaml index 89608caad7..4e85905217 100644 --- a/bindata/network/ovn-kubernetes/self-hosted/ovnkube-node.yaml +++ b/bindata/network/ovn-kubernetes/self-hosted/ovnkube-node.yaml @@ -538,6 +538,8 @@ spec: value: "{{.OVN_CONTROLLER_INACTIVITY_PROBE}}" - name: OVN_KUBE_LOG_LEVEL value: "4" + - name: OVN_NODE_MODE + value: "{{.OVN_NODE_MODE}}" {{ if .NetFlowCollectors }} - name: NETFLOW_COLLECTORS value: "{{.NetFlowCollectors}}" diff --git a/docs/architecture.md b/docs/architecture.md index 289e06a3c0..6a2339eba8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -141,6 +141,12 @@ The Network operator needs to make sure that the input configuration doesn't cha The persisted configuration must **make all defaults explicit**. This protects against inadvertent code changes that could destabilize an existing cluster. +### Per-Node Configuration + +For certain specialized deployments (e.g., DPU host nodes), some features need to be disabled on a per-node basis even when enabled cluster-wide. Since ConfigMap values cannot be reliably overridden per-node, the CNO implements per-node feature enforcement through conditional logic in the startup scripts. + +The `OVN_NODE_MODE` environment variable is injected into `ovnkube-node` pods and consumed by the startup script (`008-script-lib.yaml`) to conditionally enable or disable features based on the node's operational mode. This ensures that unsupported features are deterministically disabled on specialized hardware regardless of cluster-wide configuration. + ## Egress Router **Input:** `EgressRouter.network.operator.openshift.io` diff --git a/docs/operands.md b/docs/operands.md index 1d129a51fe..755ff81e52 100644 --- a/docs/operands.md +++ b/docs/operands.md @@ -93,6 +93,26 @@ configuration object (which in turn is copied there from the configuration) is "`OVNKubernetes`". If the specified network type is not "`OVNKubernetes`", the CNO will not render any network plugin. +### OVN-Kubernetes Node Modes + +OVN-Kubernetes supports different node operational modes through the `OVN_NODE_MODE` +environment variable. This allows per-node feature enforcement, particularly for +specialized hardware like DPU (Data Processing Unit) hosts where certain features +must be disabled. + +The startup script (`008-script-lib.yaml`) contains conditional logic that adjusts +feature enablement based on the node mode: + +- **`full` mode (default)**: All features enabled as configured +- **`dpu-host` mode**: Certain features like egress IP, multicast, multi-network + policies, and admin network policies are automatically disabled regardless of + cluster-wide configuration + +This approach was necessary because ConfigMap values (`004-config.yaml`) cannot be +reliably overridden on a per-node basis, but startup script logic can be conditional. + +For detailed information, see `docs/ovn_node_mode.md`. + ## Multus Multus is deployed as long as `.spec.disableMultiNetwork` is not set. diff --git a/docs/ovn_node_mode.md b/docs/ovn_node_mode.md new file mode 100644 index 0000000000..a0dea848e0 --- /dev/null +++ b/docs/ovn_node_mode.md @@ -0,0 +1,100 @@ +## OVN node modes and per-node feature enforcement + +This change introduces `OVN_NODE_MODE` as an environment variable injected into the `ovnkube-node` Pod. The value is consumed by the startup script rendered from `bindata/network/ovn-kubernetes/common/008-script-lib.yaml` to tailor behavior per node mode at runtime. + +### Why move flags from the config map into the script? + +- The INI-based config (`004-config.yaml`) is rendered cluster-wide. Those values are not reliably overridable on a per-node or per-mode basis. +- In DPU host mode, some features are not supported and must be deterministically disabled on those nodes even if the cluster-wide config enables them. +- Moving the enablement logic to the entrypoint script allows per-node enforcement using `OVN_NODE_MODE`, preventing unsupported features from being turned on by cluster defaults. + +### Behavior by mode + +- `full` (default): + - `gateway_interface=br-ex` + - `init_ovnkube_controller="--init-ovnkube-controller ${K8S_NODE}"` + - `enable_multicast_flag="--enable-multicast"` + - `egress_features_enable_flag="--enable-egress-ip=true --enable-egress-firewall=true --enable-egress-qos=true --enable-egress-service=true"` + - `multi_external_gateway_enable_flag="--enable-multi-external-gateway=true"` + +- `dpu-host`: + - `gateway_interface="derive-from-mgmt-port"` + - `ovnkube_node_mode="--ovnkube-node-mode dpu-host"` + - `init_ovnkube_controller=""` (disabled) + - `enable_multicast_flag=""` (disabled) + - `egress_features_enable_flag=""` (egress IP and related features disabled) + - `multi_external_gateway_enable_flag=""` (multi-external gateway disabled) + - Multi-network, network segmentation, and multi-network policy/admin network policy are gated and not enabled in this mode. + +### Manifests + +- `ovnkube-node.yaml` (managed and self-hosted) now inject `OVN_NODE_MODE` into the Pod env so the script can apply mode-aware logic. +- `ovnkube-control-plane.yaml` (managed and self-hosted) have feature flags moved from ConfigMap to inline script logic. +- `004-config.yaml` drops hard-coded feature enables that conflict with per-node enforcement. + +**Note**: Control-plane components always run in "full" mode since they don't run on DPU hosts and need all features enabled for cluster coordination. Always-enabled features (egress, multicast, multi-external-gateway) are added directly to the command line, while conditional features use script variables. + +### Implementation Details + +#### Environment Variable Injection + +The `OVN_NODE_MODE` environment variable is injected into `ovnkube-node` pods through the DaemonSet specification in both managed and self-hosted variants: + +- `bindata/network/ovn-kubernetes/managed/ovnkube-node.yaml` +- `bindata/network/ovn-kubernetes/self-hosted/ovnkube-node.yaml` + +The value is typically derived from node labels or annotations that identify the node's hardware type. + +#### Script Logic Flow + +The startup script (`008-script-lib.yaml`) implements the following conditional logic: + +```bash +if [[ "${OVN_NODE_MODE}" != "dpu-host" ]]; then + # Enable features for full mode + egress_ip_enable_flag="--enable-egress-ip=true --enable-egress-firewall=true --enable-egress-qos=true --enable-egress-service=true" + enable_multicast_flag="--enable-multicast" + # ... other feature flags +else + # DPU host mode - disable features + egress_ip_enable_flag="" + enable_multicast_flag="" + gateway_interface="derive-from-mgmt-port" + ovnkube_node_mode="--ovnkube-node-mode dpu-host" +fi +``` + +#### Feature Flag Mapping + +The following table shows how cluster-wide configuration translates to per-node enforcement: + +| Feature | ConfigMap (004-config.yaml) | Script Variable | DPU Host Behavior | +|---------|----------------------------|-----------------|-------------------| +| Egress IP | `enable-egress-ip=true` | `egress_features_enable_flag` | Force disabled | +| Multicast | `enable-multicast=true` | `enable_multicast_flag` | Force disabled | +| Multi External Gateway | `enable-multi-external-gateway=true` | `multi_external_gateway_enable_flag` | Force disabled | +| Multi-network | `enable-multi-network=true` | `multi_network_enabled_flag` | Conditionally disabled | +| Admin Network Policy | `enable-admin-network-policy=true` | `admin_network_policy_enabled_flag` | Conditionally disabled | +| Network Segmentation | `enable-network-segmentation=true` | `network_segmentation_enabled_flag` | Conditionally disabled | + +### Testing + +- Unit tests assert that the rendered script contains the correct assignments for `gateway_interface`, `init_ovnkube_controller`, `enable_multicast_flag`, `egress_features_enable_flag`, and `ovnkube_node_mode` across modes. +- The comprehensive test `TestOVNKubernetesScriptLibCombined` validates all conditional logic paths and feature flag assignments for node scripts. +- The test `TestOVNKubernetesControlPlaneFlags` validates that control-plane scripts have: + - Always-enabled features added directly to the command line (egress, multicast, multi-external-gateway) + - Conditional features handled via script variables (multi-network, network policies, etc.) + - Correct multi-network enablement logic (OVN_MULTI_NETWORK_ENABLE or OVN_NETWORK_SEGMENTATION_ENABLE) +- Tests verify both positive cases (features enabled in full mode) and negative cases (features disabled in DPU host mode). + +### Migration Notes + +When upgrading clusters that previously relied on ConfigMap-based feature control: + +1. Existing ConfigMap values in `004-config.yaml` have been removed for features that require per-node control +2. The startup scripts (both node and control-plane) now contain the authoritative feature enablement logic +3. Control-plane components automatically enable all features (always run in "full" mode) +4. DPU host nodes will automatically have incompatible features disabled regardless of previous ConfigMap settings +5. No manual intervention is required - the migration is handled automatically during the upgrade process + + diff --git a/pkg/network/ovn_kubernetes_test.go b/pkg/network/ovn_kubernetes_test.go index 8287ba8aeb..31782ac4c4 100644 --- a/pkg/network/ovn_kubernetes_test.go +++ b/pkg/network/ovn_kubernetes_test.go @@ -297,13 +297,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -340,13 +334,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=local @@ -396,14 +384,8 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-reachability-total-timeout=3 egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=local @@ -455,14 +437,8 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-reachability-total-timeout=0 egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=local @@ -514,13 +490,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=local @@ -572,13 +542,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -619,13 +583,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -669,13 +627,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -712,12 +664,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-external-gateway=true [gateway] mode=shared @@ -755,15 +702,8 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true enable-multi-networkpolicy=true -enable-admin-network-policy=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -802,14 +742,8 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true enable-network-segmentation=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -846,13 +780,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-admin-network-policy=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -891,13 +819,7 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true -enable-multi-external-gateway=true enable-dns-name-resolver=true [gateway] @@ -935,15 +857,9 @@ dns-service-namespace="openshift-dns" dns-service-name="dns-default" [ovnkubernetesfeature] -enable-egress-ip=true -enable-egress-firewall=true -enable-egress-qos=true -enable-egress-service=true egressip-node-healthcheck-port=9107 -enable-multi-network=true enable-network-segmentation=true enable-preconfigured-udn-addresses=true -enable-multi-external-gateway=true [gateway] mode=shared @@ -3989,6 +3905,92 @@ func extractOVNScriptLib(g *WithT, objs []*uns.Unstructured) string { return "" } +// renderControlPlaneWithOverrides renders using the full render path and returns +// the embedded startup script from the control-plane container. +func renderControlPlaneWithOverrides(t *testing.T, variant string, overrides map[string]interface{}) string { + g := NewGomegaWithT(t) + crd := OVNKubernetesConfig.DeepCopy() + config := &crd.Spec + fillDefaults(config, nil) + + bs := fakeBootstrapResult() + bs.OVN = bootstrap.OVNBootstrapResult{ + ControlPlaneReplicaCount: 1, + OVNKubernetesConfig: &bootstrap.OVNConfigBoostrapResult{ + DpuHostModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU_HOST, + DpuModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU, + SmartNicModeLabel: OVN_NODE_SELECTOR_DEFAULT_SMART_NIC, + HyperShiftConfig: &bootstrap.OVNHyperShiftBootstrapResult{Enabled: false}, + ConfigOverrides: toStringMap(overrides), + }, + } + + featureGatesCNO := getDefaultFeatureGates() + fakeClient := cnofake.NewFakeClient() + objs, _, err := renderOVNKubernetes(config, bs, manifestDirOvn, fakeClient, featureGatesCNO) + g.Expect(err).NotTo(HaveOccurred()) + + var script string + for _, obj := range objs { + if obj.GetKind() == "Deployment" && obj.GetName() == "ovnkube-control-plane" && obj.GetNamespace() == "openshift-ovn-kubernetes" { + containers, found, err := uns.NestedSlice(obj.Object, "spec", "template", "spec", "containers") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + for _, c := range containers { + cm := c.(map[string]interface{}) + if name, ok := cm["name"]; ok && (name == "ovnkube-cluster-manager" || name == "ovnkube-control-plane") { + command, found, err := uns.NestedSlice(cm, "command") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + g.Expect(len(command)).To(BeNumerically(">", 2)) + script = command[2].(string) + break + } + } + } + } + g.Expect(script).NotTo(BeEmpty()) + return script +} + +// renderScriptLibWithOverrides renders using the full render path and returns +// the ovnkube script-lib content. +func renderScriptLibWithOverrides(t *testing.T, overrides map[string]interface{}) string { + g := NewGomegaWithT(t) + crd := OVNKubernetesConfig.DeepCopy() + config := &crd.Spec + fillDefaults(config, nil) + + bs := fakeBootstrapResult() + bs.OVN = bootstrap.OVNBootstrapResult{ + ControlPlaneReplicaCount: 1, + OVNKubernetesConfig: &bootstrap.OVNConfigBoostrapResult{ + DpuHostModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU_HOST, + DpuModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU, + SmartNicModeLabel: OVN_NODE_SELECTOR_DEFAULT_SMART_NIC, + HyperShiftConfig: &bootstrap.OVNHyperShiftBootstrapResult{Enabled: false}, + ConfigOverrides: toStringMap(overrides), + }, + } + featureGatesCNO := getDefaultFeatureGates() + fakeClient := cnofake.NewFakeClient() + objs, _, err := renderOVNKubernetes(config, bs, manifestDirOvn, fakeClient, featureGatesCNO) + g.Expect(err).NotTo(HaveOccurred()) + return extractOVNScriptLib(g, objs) +} + +// helper to convert map[string]interface{} -> map[string]string for overrides +func toStringMap(in map[string]interface{}) map[string]string { + if in == nil { + return nil + } + out := make(map[string]string, len(in)) + for k, v := range in { + out[k] = fmt.Sprintf("%v", v) + } + return out +} + // checkDaemonsetAnnotation check that all the daemonset have the annotation with the // same key and value func checkDaemonsetAnnotation(g *WithT, objs []*uns.Unstructured, key, value string) bool { @@ -4245,3 +4247,227 @@ func TestRenderOVNKubernetes_AdvertisedUDNIsolationModeOverride(t *testing.T) { g.Expect(ovnkubeScriptLib).To(ContainSubstring(`--advertised-udn-isolation-mode="`)) }) } + +func TestOVNKubernetesControlPlaneFlags(t *testing.T) { + g := NewGomegaWithT(t) + + testCases := []struct { + name string + variant string + overrides map[string]interface{} + mustContain []string + mustNotContain []string + }{ + { + name: "self-hosted control-plane: always-enabled features", + variant: "self-hosted", + overrides: map[string]interface{}{ + "OVN_OBSERVABILITY_ENABLE": "false", + "OVN_MULTI_NETWORK_POLICY_ENABLE": "false", + "OVN_ADMIN_NETWORK_POLICY_ENABLE": "false", + }, + mustContain: []string{ + "--enable-egress-ip=true", + "--enable-egress-firewall=true", + "--enable-egress-qos=true", + "--enable-egress-service=true", + "--enable-multicast", + "--enable-multi-external-gateway=true", + }, + mustNotContain: []string{ + "egress_features_enable_flag=", + "enable_multicast_flag=", + "multi_external_gateway_enable_flag=", + }, + }, + + { + name: "self-hosted control-plane: conditional features enabled", + variant: "self-hosted", + overrides: map[string]interface{}{ + "OVN_MULTI_NETWORK_POLICY_ENABLE": "true", + "OVN_ADMIN_NETWORK_POLICY_ENABLE": "true", + }, + mustContain: []string{ + "--enable-egress-ip=true", + "--enable-multicast", + "--enable-multi-external-gateway=true", + "multi_network_policy_enabled_flag=\"--enable-multi-networkpolicy\"", + "admin_network_policy_enabled_flag=\"--enable-admin-network-policy\"", + }, + mustNotContain: []string{ + "network_observability_enabled_flag=", + }, + }, + { + name: "self-hosted control-plane: multi-network enabled", + variant: "self-hosted", + overrides: map[string]interface{}{ + "OVN_MULTI_NETWORK_ENABLE": "true", + }, + mustContain: []string{ + "--enable-egress-ip=true", + "--enable-multicast", + "--enable-multi-external-gateway=true", + "multi_network_enabled_flag=\"--enable-multi-network\"", + }, + mustNotContain: []string{}, + }, + { + name: "self-hosted control-plane: network segmentation enabled (auto-enables multi-network)", + variant: "self-hosted", + overrides: map[string]interface{}{ + "OVN_NETWORK_SEGMENTATION_ENABLE": "true", + "OVN_MULTI_NETWORK_ENABLE": "false", + }, + mustContain: []string{ + "--enable-egress-ip=true", + "--enable-multicast", + "--enable-multi-external-gateway=true", + "multi_network_enabled_flag=\"--enable-multi-network\"", + "network_segmentation_enabled_flag=\"--enable-network-segmentation\"", + }, + mustNotContain: []string{}, + }, + { + name: "self-hosted control-plane: both multi-network and segmentation enabled", + variant: "self-hosted", + overrides: map[string]interface{}{ + "OVN_NETWORK_SEGMENTATION_ENABLE": "true", + "OVN_MULTI_NETWORK_ENABLE": "true", + }, + mustContain: []string{ + "--enable-egress-ip=true", + "--enable-multicast", + "--enable-multi-external-gateway=true", + "multi_network_enabled_flag=\"--enable-multi-network\"", + "network_segmentation_enabled_flag=\"--enable-network-segmentation\"", + }, + mustNotContain: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + script := renderControlPlaneWithOverrides(t, tc.variant, tc.overrides) + for _, s := range tc.mustContain { + g.Expect(script).To(ContainSubstring(s), "Expected to find: %s", s) + } + for _, s := range tc.mustNotContain { + g.Expect(script).NotTo(ContainSubstring(s), "Expected NOT to find: %s", s) + } + }) + } +} + +func TestOVNKubernetesScriptLibCombined(t *testing.T) { + g := NewGomegaWithT(t) + + renderScript := func(overrides map[string]interface{}) string { + return renderScriptLibWithOverrides(t, overrides) + } + + testCases := []struct { + name string + overrides map[string]interface{} + mustContain []string + mustNotContain []string + }{ + { + name: "dpu-host gating and egress/policy disable", + overrides: map[string]interface{}{ + "OVN_NODE_MODE": "dpu-host", + "OVN_MULTI_NETWORK_ENABLE": "true", + "OVN_NETWORK_SEGMENTATION_ENABLE": "true", + "OVN_MULTI_NETWORK_POLICY_ENABLE": "true", + "OVN_ADMIN_NETWORK_POLICY_ENABLE": "true", + }, + mustContain: []string{ + "gateway_interface=\"derive-from-mgmt-port\"", + "init_ovnkube_controller=\"\"", + "enable_multicast_flag=\"\"", + "egress_features_enable_flag=\"\"", + "multi_external_gateway_enable_flag=\"\"", + "ovnkube_node_mode=\"--ovnkube-node-mode dpu-host\"", + "multi_network_enabled_flag=", + "network_segmentation_enabled_flag=", + "multi_network_policy_enabled_flag=", + "admin_network_policy_enabled_flag=", + }, + mustNotContain: []string{}, + }, + { + name: "full mode with multi-network features enabled", + overrides: map[string]interface{}{ + "OVN_NODE_MODE": "full", + "OVN_MULTI_NETWORK_ENABLE": "true", + "OVN_NETWORK_SEGMENTATION_ENABLE": "true", + "OVN_MULTI_NETWORK_POLICY_ENABLE": "true", + "OVN_ADMIN_NETWORK_POLICY_ENABLE": "true", + }, + mustContain: []string{ + "gateway_interface=br-ex", + "init_ovnkube_controller=\"--init-ovnkube-controller ${K8S_NODE}\"", + "enable_multicast_flag=\"--enable-multicast\"", + "egress_features_enable_flag=\"--enable-egress-ip=true --enable-egress-firewall=true --enable-egress-qos=true --enable-egress-service=true\"", + "multi_external_gateway_enable_flag=\"--enable-multi-external-gateway=true\"", + "multi_network_enabled_flag=\"--enable-multi-network\"", + "network_segmentation_enabled_flag=\"--enable-network-segmentation\"", + "multi_network_policy_enabled_flag=\"--enable-multi-networkpolicy\"", + "admin_network_policy_enabled_flag=\"--enable-admin-network-policy\"", + }, + mustNotContain: []string{}, + }, + { + name: "non-mode-gated features enabled", + overrides: map[string]interface{}{ + "OVN_NODE_MODE": "full", + "OVN_ROUTE_ADVERTISEMENTS_ENABLE": "true", + "OVN_PRE_CONF_UDN_ADDR_ENABLE": "true", + "OVN_OBSERVABILITY_ENABLE": "true", + "DNS_NAME_RESOLVER_ENABLE": "true", + "NETWORK_NODE_IDENTITY_ENABLE": "true", + }, + mustContain: []string{ + "route_advertisements_enable_flag=\"--enable-route-advertisements\"", + "preconfigured_udn_addresses_enable_flag=\"--enable-preconfigured-udn-addresses\"", + "network_observability_enabled_flag=\"--enable-observability\"", + "dns_name_resolver_enabled_flag=\"--enable-dns-name-resolver\"", + "ip_forwarding_flag=\"--disable-forwarding\"", + "--bootstrap-kubeconfig=/var/lib/kubelet/kubeconfig", + }, + mustNotContain: []string{}, + }, + { + name: "full mode: multi-network features disabled", + overrides: map[string]interface{}{ + "OVN_NODE_MODE": "full", + "OVN_MULTI_NETWORK_ENABLE": "false", + "OVN_NETWORK_SEGMENTATION_ENABLE": "false", + "OVN_MULTI_NETWORK_POLICY_ENABLE": "false", + "OVN_ADMIN_NETWORK_POLICY_ENABLE": "false", + }, + mustContain: []string{ + "multi_network_enabled_flag=", + "network_segmentation_enabled_flag=", + "multi_network_policy_enabled_flag=", + "admin_network_policy_enabled_flag=", + }, + mustNotContain: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + script := renderScript(tc.overrides) + for _, s := range tc.mustContain { + g.Expect(script).To(ContainSubstring(s)) + } + for _, s := range tc.mustNotContain { + g.Expect(script).NotTo(ContainSubstring(s)) + } + // Ensure gateway flags use the variable rather than a hardcoded iface + g.Expect(script).To(ContainSubstring("--gateway-interface ${gateway_interface}")) + }) + } +}