From 88f6a1a39c0624ffc4677f1859fbf1b3a785b561 Mon Sep 17 00:00:00 2001 From: Raphael Rosa Date: Wed, 27 May 2026 10:10:31 +0200 Subject: [PATCH] OCPBUGS-62144: Restart pods when rendering egressIP settings Issue: EgressIP ReachabilityTotalTimeoutSeconds is rendered by ovnkube-config (configmap), but ovnkube node and control pods are not restarted to have the new value applied in their settings. Solution: Make sure the spec templates of ovnkube node and control-plane pods take into account the changes in the ReachabilityTotalTimeoutSeconds setting. This is implemented by rendering the cli flags in their spec template command. When this value is changed, it then triggers the restart of those pods, reloading the parameter in their configuration. Adds test cases to evaluate the rendering and the hash changes in ovnkube node and control pod specs. Updates README to document that reachabilityTimeoutSeconds is enabled for runtime updates. Signed-off-by: Raphael Rosa --- README.md | 2 +- .../ovn-kubernetes/common/008-script-lib.yaml | 8 +- .../managed/ovnkube-control-plane.yaml | 8 +- .../self-hosted/004-config.yaml | 3 - .../self-hosted/ovnkube-control-plane.yaml | 8 +- pkg/network/ovn_kubernetes_test.go | 147 +++++++++++++++++- 6 files changed, 167 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ad87d60f85..6256647bd3 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ Other values are ignored. If you wish to use use a third-party network provider ### Configuring OVNKubernetes -OVNKubernetes supports the following configuration options, all of which are optional and once set at cluster creation, they can't be changed except for `gatewayConfig` and `IPsec` which can be changed at runtime: +OVNKubernetes supports the following configuration options, all of which are optional and once set at cluster creation, they can't be changed except for `gatewayConfig`, `IPsec` and `reachabilityTotalTimeoutSeconds` which can be changed at runtime: * `MTU`: The MTU to use for the geneve overlay. The default is the MTU of the node that the cluster-network-operator is first run on, minus 100 bytes for geneve overhead. If the nodes in your cluster don't all have the same MTU then you may need to set this explicitly. * `genevePort`: The UDP port to use for the Geneve overlay. The default is 6081. * `hybridOverlayConfig`: hybrid linux/windows cluster (see below). diff --git a/bindata/network/ovn-kubernetes/common/008-script-lib.yaml b/bindata/network/ovn-kubernetes/common/008-script-lib.yaml index 8276c773d5..b4c8b1840f 100644 --- a/bindata/network/ovn-kubernetes/common/008-script-lib.yaml +++ b/bindata/network/ovn-kubernetes/common/008-script-lib.yaml @@ -569,6 +569,11 @@ data: echo "I$(date "+%m%d %H:%M:%S.%N") - starting ovnkube-node" + ovn_eip_reachability_timeout_opt= + {{- if .ReachabilityTotalTimeoutSeconds }} + ovn_eip_reachability_timeout_opt="--egressip-reachability-total-timeout {{.ReachabilityTotalTimeoutSeconds}}" + {{ end }} + init_ovnkube_controller="--init-ovnkube-controller ${K8S_NODE}" gateway_interface="br-ex" OVN_NODE_MODE=${OVN_NODE_MODE:-full} @@ -746,5 +751,6 @@ 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} \ + ${ovn_eip_reachability_timeout_opt} } diff --git a/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml b/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml index 55817fe7ce..2bf64ccc55 100644 --- a/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml +++ b/bindata/network/ovn-kubernetes/managed/ovnkube-control-plane.yaml @@ -156,6 +156,11 @@ spec: fi done + ovn_eip_reachability_timeout_opt= + {{- if .ReachabilityTotalTimeoutSeconds }} + ovn_eip_reachability_timeout_opt="--egressip-reachability-total-timeout {{.ReachabilityTotalTimeoutSeconds}}" + {{ end }} + ovn_v4_join_subnet_opt= if [[ "{{.V4JoinSubnet}}" != "" ]]; then ovn_v4_join_subnet_opt="--gateway-v4-join-subnet {{.V4JoinSubnet}}" @@ -222,7 +227,8 @@ spec: ${ovn_v6_masquerade_subnet_opt} \ ${persistent_ips_enabled_flag} \ ${route_advertisements_enable_flag} \ - ${evpn_enable_flag} + ${evpn_enable_flag} \ + ${ovn_eip_reachability_timeout_opt} volumeMounts: - mountPath: /run/ovnkube-config/ name: ovnkube-config diff --git a/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml b/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml index 6c1944f7b8..23e3b1c589 100644 --- a/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml +++ b/bindata/network/ovn-kubernetes/self-hosted/004-config.yaml @@ -40,9 +40,6 @@ data: 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 }} 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 3521ed08ac..2f69ef19ef 100644 --- a/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml +++ b/bindata/network/ovn-kubernetes/self-hosted/ovnkube-control-plane.yaml @@ -102,6 +102,11 @@ spec: source "/env/_master" set +o allexport fi + + ovn_eip_reachability_timeout_opt= + {{- if .ReachabilityTotalTimeoutSeconds }} + ovn_eip_reachability_timeout_opt="--egressip-reachability-total-timeout {{.ReachabilityTotalTimeoutSeconds}}" + {{ end }} ovn_v4_join_subnet_opt= if [[ "{{.V4JoinSubnet}}" != "" ]]; then @@ -176,7 +181,8 @@ spec: ${persistent_ips_enabled_flag} \ ${gateway_mode_flags} \ ${route_advertisements_enable_flag} \ - ${evpn_enable_flag} + ${evpn_enable_flag} \ + ${ovn_eip_reachability_timeout_opt} volumeMounts: - mountPath: /run/ovnkube-config/ name: ovnkube-config diff --git a/pkg/network/ovn_kubernetes_test.go b/pkg/network/ovn_kubernetes_test.go index e091eff928..91c6a5eb0a 100644 --- a/pkg/network/ovn_kubernetes_test.go +++ b/pkg/network/ovn_kubernetes_test.go @@ -405,7 +405,6 @@ 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-network-segmentation=true @@ -467,7 +466,6 @@ 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-network-segmentation=true @@ -4001,6 +3999,151 @@ func TestRenderOVNKubernetesEnablePersistentIPs(t *testing.T) { g.Expect(objs).To(ContainElement(HaveKubernetesID("CustomResourceDefinition", "", "ipamclaims.k8s.cni.cncf.io"))) } +// TestRenderOVNKubernetesReachability tests egress IP reachability timeout rendering +func TestRenderOVNKubernetesReachability(t *testing.T) { + g := NewGomegaWithT(t) + + testCases := []struct { + name string + reachabilityTimeout *uint32 + expectKubernetesFeatureReachability bool + expectErr bool + }{ + { + name: "No reachability timeout (nil)", + reachabilityTimeout: nil, + expectKubernetesFeatureReachability: false, + expectErr: false, + }, + { + name: "Reachability timeout set to 0", + reachabilityTimeout: ptrToUint32(0), + expectKubernetesFeatureReachability: true, + expectErr: false, + }, + { + name: "Reachability timeout changed to 10", + reachabilityTimeout: ptrToUint32(10), + expectKubernetesFeatureReachability: true, + expectErr: false, + }, + { + name: "Reachability timeout unchanged to 10", + reachabilityTimeout: ptrToUint32(10), + expectKubernetesFeatureReachability: true, + expectErr: false, + }, + { + name: "Reachability timeout changed to 5", + reachabilityTimeout: ptrToUint32(5), + expectKubernetesFeatureReachability: true, + expectErr: false, + }, + { + name: "Reachability timeout disabled", + reachabilityTimeout: nil, + expectKubernetesFeatureReachability: false, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + crd := OVNKubernetesConfig.DeepCopy() + config := &crd.Spec + config.DefaultNetwork.OVNKubernetesConfig.EgressIPConfig.ReachabilityTotalTimeoutSeconds = tc.reachabilityTimeout + + errs := validateOVNKubernetes(config) + g.Expect(errs).To(HaveLen(0)) + fillDefaults(config, nil) + + // at the same time we have an upgrade + t.Setenv("RELEASE_VERSION", "2.0.0") + + bootstrapResult := fakeBootstrapResult() + bootstrapResult.OVN = bootstrap.OVNBootstrapResult{ + ControlPlaneReplicaCount: 3, + OVNKubernetesConfig: &bootstrap.OVNConfigBoostrapResult{ + DpuHostModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU_HOST, + DpuModeLabel: OVN_NODE_SELECTOR_DEFAULT_DPU, + SmartNicModeLabel: OVN_NODE_SELECTOR_DEFAULT_SMART_NIC, + MgmtPortResourceName: "", + HyperShiftConfig: &bootstrap.OVNHyperShiftBootstrapResult{ + Enabled: false, + }, + }, + } + + featureGatesCNO := getDefaultFeatureGates() + fakeClient := cnofake.NewFakeClient() + // Set is as Hypershift hosted control plane. + bootstrapResult.Infra = bootstrap.InfraStatus{} + bootstrapResult.Infra.HostedControlPlane = &hypershift.HostedControlPlane{} + objs, _, err := renderOVNKubernetes(config, bootstrapResult, manifestDirOvn, fakeClient, featureGatesCNO) + if tc.expectErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + var configMap *uns.Unstructured + var scriptCP string + var scriptNode string + for _, obj := range objs { + // Gets script that starts ovnkube-control-plane pod, ovnkube-cluster-manager container + 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" { + command, found, err := uns.NestedSlice(cm, "command") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + g.Expect(len(command)).To(BeNumerically(">", 2)) // Command options are set in the 3rd slice + scriptCP = command[2].(string) + break + } + } + } + + // Gets script that starts ovnkube-node pod, ovnkube-controller container + if obj.GetKind() == "ConfigMap" && obj.GetName() == "ovnkube-script-lib" { + configMap = obj + configMapData, found, err := uns.NestedStringMap(configMap.Object, "data") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue(), "ovnkube-script-lib ConfigMap should have data field") + scriptNode = configMapData["ovnkube-lib.sh"] + } + } + g.Expect(scriptCP).NotTo(BeEmpty()) + g.Expect(scriptNode).NotTo(BeEmpty()) + + if tc.expectKubernetesFeatureReachability { + g.Expect(scriptCP).To( + ContainSubstring(fmt.Sprintf("--egressip-reachability-total-timeout %d", *tc.reachabilityTimeout)), + "ovnkube-control-plane pod template should contain the configured reachability timeout value", + ) + g.Expect(scriptNode).To( + ContainSubstring(fmt.Sprintf("--egressip-reachability-total-timeout %d", *tc.reachabilityTimeout)), + "ovnkube-node pod template should contain the configured reachability timeout value", + ) + + } else { + g.Expect(scriptCP).NotTo( + ContainSubstring("--egressip-reachability-total-timeout"), + "ovnkube-control-plane pod template should not contain the configured reachability timeout value", + ) + g.Expect(scriptNode).NotTo( + ContainSubstring("--egressip-reachability-total-timeout"), + "ovnkube-node pod template should not contain the configured reachability timeout value", + ) + } + }) + } +} + type fakeClientReader struct { configMap *v1.ConfigMap }