Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 282 additions & 0 deletions test/extended/networking/egressip.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"os"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -567,6 +568,287 @@ var _ = g.Describe("[sig-network][Feature:EgressIP][apigroup:operator.openshift.
}) // end testing to external targets
})

var _ = g.Describe("[sig-network][Feature:EgressIP][apigroup:operator.openshift.io] EgressIP duplicate MAC prevention", func() {
oc := exutil.NewCLIWithPodSecurityLevel("egressip-mac", admissionapi.LevelPrivileged)

const (
egressIPObjectName = "egressip-mac-test"
)

var (
clientset kubernetes.Interface
tmpDirEgressIP string
workerNodesOrdered []corev1.Node
workerNodesOrderedNames []string
hasIPv4 bool
hasIPv6 bool
)

g.BeforeEach(func() {
g.By("Verifying that this cluster uses OVN-Kubernetes")
if networkPluginName() != OVNKubernetesPluginName {
skipper.Skipf("This cluster does not use OVN Kubernetes")
}

g.By("Checking platform type - this test requires L2 network adjacency")
infra, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(context.Background(), "cluster", metav1.GetOptions{})
o.Expect(err).NotTo(o.HaveOccurred())
cloudType := infra.Spec.PlatformSpec.Type
cloudPlatforms := []configv1.PlatformType{
configv1.AWSPlatformType,
configv1.GCPPlatformType,
configv1.AzurePlatformType,
configv1.OpenStackPlatformType,
}
for _, cp := range cloudPlatforms {
if cloudType == cp {
skipper.Skipf("This test requires L2 network adjacency (baremetal); cloud platform %s is not supported", cloudType)
}
}
Comment on lines +593 to +607

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare Spec.PlatformSpec.Type vs Status.PlatformStatus.Type usage in this repo's tests.
rg -nP --type=go -C2 '(Spec\.PlatformSpec\.Type|Status\.PlatformStatus\.Type)' test/ | head -80

Repository: openshift/origin

Length of output: 5204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- target file context ---\n'
sed -n '550,630p' test/extended/networking/egressip.go

printf '\n--- Infrastructure definitions and usages in egressip.go ---\n'
rg -n --type=go -C3 'Infrastructure|Infrastructures|Spec\.PlatformSpec|Status\.PlatformStatus' test/extended/networking/egressip.go

printf '\n--- focused repo usage: Infrastructures and PlatformStatus vs Spec.PlatformSpec ---\n'
rg -n --type=go -C2 'AdminConfigClient\(\)\.ConfigV1\(\)\.Infrastructures\(\)\.Get|Status\.PlatformStatus\.Type|Spec\.PlatformSpec\.Type' test | head -200

Repository: openshift/origin

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target file context ---'
sed -n '550,630p' test/extended/networking/egressip.go

printf '%s\n' ''
printf '%s\n' '--- Infrastructure definitions and usages in egressip.go ---'
rg -n --type=go -C3 'Infrastructure|Infrastructures|Spec\.PlatformSpec|Status\.PlatformStatus' test/extended/networking/egressip.go

printf '%s\n' ''
printf '%s\n' '--- focused repo usage: Infrastructures and PlatformStatus vs Spec.PlatformSpec ---'
rg -n --type=go -C2 'AdminConfigClient\(\)\.ConfigV1\(\)\.Infrastructures\(\)\.Get|Status\.PlatformStatus\.Type|Spec\.PlatformSpec\.Type' test | head -200

Repository: openshift/origin

Length of output: 21107


Read the platform type from Status.PlatformStatus, not Spec.PlatformSpec.

Spec.PlatformSpec.Type can remain empty on installed clusters, so cloud platforms may not match the skip list and the test can run where L2 adjacency is unavailable. Use infra.Status.PlatformStatus.Type, with a nil PlatformStatus guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 593 - 607, Update the
platform check in the egress IP test to read the type from
infra.Status.PlatformStatus.Type instead of infra.Spec.PlatformSpec.Type. Guard
against a nil PlatformStatus and preserve the existing cloud-platform skip
behavior when the status type matches an unsupported platform.


g.By("Creating a temp directory")
tmpDirEgressIP, err = ioutil.TempDir("", "egressip-mac-e2e")
o.Expect(err).NotTo(o.HaveOccurred())

g.By("Getting the kubernetes clientset")
clientset = oc.KubeFramework().ClientSet

g.By("Getting all worker nodes in alphabetical order")
workerNodesOrdered, err = getWorkerNodesOrdered(clientset)
o.Expect(err).NotTo(o.HaveOccurred())
workerNodesOrderedNames = nil
for _, n := range workerNodesOrdered {
workerNodesOrderedNames = append(workerNodesOrderedNames, n.Name)
}
if len(workerNodesOrdered) < 3 {
skipper.Skipf("This test requires minimum 3 worker nodes, found %d", len(workerNodesOrdered))
}

g.By("Determining the IP address families")
hasIPv4, hasIPv6, err = GetIPAddressFamily(oc)
o.Expect(err).NotTo(o.HaveOccurred())
})

g.AfterEach(func() {
g.By("Deleting the EgressIP object if it exists")
egressIPYamlPath := tmpDirEgressIP + "/" + egressIPYaml
if _, err := os.Stat(egressIPYamlPath); err == nil {
_, _ = runOcWithRetry(oc.AsAdmin(), "delete", "-f", egressIPYamlPath)
}

g.By("Removing the egress-assignable labels from all worker nodes")
for _, nodeName := range workerNodesOrderedNames {
_, _ = runOcWithRetry(oc.AsAdmin(), "label", "node", nodeName, "k8s.ovn.org/egress-assignable-")
}

g.By("Removing the temp directory")
os.RemoveAll(tmpDirEgressIP)
})

g.It("should prevent duplicate MAC responses when egress node is rebooted [Serial]", func() {
// Node assignment:
// workerNodesOrderedNames[0] = probe node (runs arping, NOT egress-assignable)
// workerNodesOrderedNames[1] = egress node 1 (initial EgressIP holder)
// workerNodesOrderedNames[2] = egress node 2 (failover target)
probeNodeName := workerNodesOrderedNames[0]
egressNode1Name := workerNodesOrderedNames[1]
egressNode2Name := workerNodesOrderedNames[2]

isIPv6 := hasIPv6 && !hasIPv4

g.By("1. Labeling egress node 1 as egress-assignable")
_, err := runOcWithRetry(oc.AsAdmin(), "label", "node", egressNode1Name, "k8s.ovn.org/egress-assignable=")
o.Expect(err).NotTo(o.HaveOccurred())

g.By("2. Allocating an EgressIP from egress node 1")
nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name})
o.Expect(err).NotTo(o.HaveOccurred())
egressIPStr := nodeEgressIPMap[egressNode1Name][0]
framework.Logf("Allocated EgressIP: %s for node %s", egressIPStr, egressNode1Name)
Comment on lines +664 to +667

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Assert that an EgressIP was allocated before indexing.

Line 666 indexes nodeEgressIPMap[egressNode1Name][0] without a length check. If findNodeEgressIPsBaremetal returns no address for the node, the test panics with an index-out-of-range error instead of reporting a clear failure.

🛡️ Proposed change
 		nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name})
 		o.Expect(err).NotTo(o.HaveOccurred())
+		o.Expect(nodeEgressIPMap).To(o.HaveKey(egressNode1Name))
+		o.Expect(nodeEgressIPMap[egressNode1Name]).NotTo(o.BeEmpty(),
+			fmt.Sprintf("no free EgressIP found for node %s", egressNode1Name))
 		egressIPStr := nodeEgressIPMap[egressNode1Name][0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name})
o.Expect(err).NotTo(o.HaveOccurred())
egressIPStr := nodeEgressIPMap[egressNode1Name][0]
framework.Logf("Allocated EgressIP: %s for node %s", egressIPStr, egressNode1Name)
nodeEgressIPMap, err := findNodeEgressIPsBaremetal(oc, clientset, []string{egressNode1Name})
o.Expect(err).NotTo(o.HaveOccurred())
o.Expect(nodeEgressIPMap).To(o.HaveKey(egressNode1Name))
o.Expect(nodeEgressIPMap[egressNode1Name]).NotTo(o.BeEmpty(),
fmt.Sprintf("no free EgressIP found for node %s", egressNode1Name))
egressIPStr := nodeEgressIPMap[egressNode1Name][0]
framework.Logf("Allocated EgressIP: %s for node %s", egressIPStr, egressNode1Name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 664 - 667, In the EgressIP
validation flow after findNodeEgressIPsBaremetal returns, assert that
nodeEgressIPMap[egressNode1Name] contains at least one address before indexing
element 0. Keep the existing error assertion and logging behavior, but report a
clear test failure when no EgressIP was allocated instead of allowing an
index-out-of-range panic.


g.By("3. Creating and applying the EgressIP object")
egressIPYamlPath := tmpDirEgressIP + "/" + egressIPYaml
egressIPSet := map[string]string{egressIPStr: egressNode1Name}
createEgressIPObject(oc, egressIPYamlPath, egressIPObjectName, oc.Namespace(), "", egressIPSet)
_, err = runOcWithRetry(oc.AsAdmin(), "create", "-f", egressIPYamlPath)
o.Expect(err).NotTo(o.HaveOccurred())

g.By("4. Verifying EgressIP is assigned to egress node 1")
var hasIP bool
var assignedNode string
o.Eventually(func() bool {
hasIP, assignedNode, err = egressIPStatusHasIP(oc, egressIPObjectName, egressIPStr)
if err != nil {
framework.Logf("Error checking EgressIP status: %v", err)
return false
}
return hasIP && assignedNode == egressNode1Name
}, 60*time.Second, 5*time.Second).Should(o.BeTrue(),
fmt.Sprintf("EgressIP %s should be assigned to node %s", egressIPStr, egressNode1Name))
framework.Logf("EgressIP %s assigned to node: %s", egressIPStr, assignedNode)

g.By("5. Labeling egress node 2 as egress-assignable for failover")
_, err = runOcWithRetry(oc.AsAdmin(), "label", "node", egressNode2Name, "k8s.ovn.org/egress-assignable=")
o.Expect(err).NotTo(o.HaveOccurred())

g.By("6. Getting br-ex physical interface name")
physInterface, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex")
o.Expect(err).NotTo(o.HaveOccurred())
framework.Logf("Using physical interface: %s", physInterface)

g.By("7. Getting MAC addresses of egress node 1 and egress node 2")
mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, physInterface)
o.Expect(err).NotTo(o.HaveOccurred())
mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, physInterface)
o.Expect(err).NotTo(o.HaveOccurred())
framework.Logf("Egress node 1 (%s) MAC: %s", egressNode1Name, mac1)
framework.Logf("Egress node 2 (%s) MAC: %s", egressNode2Name, mac2)
Comment on lines +694 to +705

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Resolve the physical interface name per node.

Line 695 discovers the br-ex physical interface on egressNode1Name only. The test then reuses that name for egressNode2Name (line 702) and for the probe node (lines 717 and 802). On bare metal, NIC names can differ between nodes, for example eno1 on one node and enp2s0f0 on another. The MAC lookup or the probe then fails for a reason that is unrelated to the behavior under test.

Call findBridgePhysicalInterface for each node and use the matching name in each command.

🐛 Proposed change
-		g.By("6. Getting br-ex physical interface name")
-		physInterface, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex")
-		o.Expect(err).NotTo(o.HaveOccurred())
-		framework.Logf("Using physical interface: %s", physInterface)
-
-		g.By("7. Getting MAC addresses of egress node 1 and egress node 2")
-		mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, physInterface)
-		o.Expect(err).NotTo(o.HaveOccurred())
-		mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, physInterface)
-		o.Expect(err).NotTo(o.HaveOccurred())
+		g.By("6. Getting br-ex physical interface name on each node")
+		iface1, err := findBridgePhysicalInterface(oc, egressNode1Name, "br-ex")
+		o.Expect(err).NotTo(o.HaveOccurred())
+		iface2, err := findBridgePhysicalInterface(oc, egressNode2Name, "br-ex")
+		o.Expect(err).NotTo(o.HaveOccurred())
+		probeInterface, err := findBridgePhysicalInterface(oc, probeNodeName, "br-ex")
+		o.Expect(err).NotTo(o.HaveOccurred())
+
+		g.By("7. Getting MAC addresses of egress node 1 and egress node 2")
+		mac1, err := getNodeInterfaceMAC(oc, egressNode1Name, iface1)
+		o.Expect(err).NotTo(o.HaveOccurred())
+		mac2, err := getNodeInterfaceMAC(oc, egressNode2Name, iface2)
+		o.Expect(err).NotTo(o.HaveOccurred())

Then pass probeInterface to the discovery command at line 717 and to checkForDuplicateMACOnNode at line 802.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 694 - 705, Update the
networking test flow around findBridgePhysicalInterface to resolve and retain
separate physical interface names for egressNode1Name, egressNode2Name, and the
probe node. Use each node’s matching interface for getNodeInterfaceMAC, the
discovery command, and checkForDuplicateMACOnNode instead of reusing
egressNode1Name’s interface.


g.By("8. Verifying EgressIP resolves to egress node 1 MAC before migration")
probePodInfo, err := ovnkubePod(oc, probeNodeName)
o.Expect(err).NotTo(o.HaveOccurred())

var discoveryCmd string
var macRegex *regexp.Regexp
if isIPv6 {
discoveryCmd = fmt.Sprintf("ndisc6 -1 -w 1000 %s %s 2>&1", egressIPStr, physInterface)
macRegex = regexp.MustCompile(`Target link-layer address:\s+([0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2}:[0-9a-fA-F]{1,2})`)
} else {
discoveryCmd = fmt.Sprintf("arping -c 1 -I %s %s 2>&1", physInterface, egressIPStr)
macRegex = regexp.MustCompile(`\[([0-9a-fA-F:]+)\]`)
}
output, err := adminExecInPod(oc, "openshift-ovn-kubernetes", probePodInfo.podName, probePodInfo.containerName, discoveryCmd)
o.Expect(err).NotTo(o.HaveOccurred(), "network discovery should succeed before migration")
matches := macRegex.FindStringSubmatch(output)
o.Expect(matches).To(o.HaveLen(2), fmt.Sprintf("should extract MAC from discovery output: %s", output))
macBeforeMigration := strings.ToLower(strings.TrimSpace(matches[1]))
framework.Logf("MAC before migration: %s, expected node 1 MAC: %s", macBeforeMigration, mac1)
o.Expect(macBeforeMigration).To(o.Equal(mac1), "EgressIP should resolve to egress node 1 MAC before migration")
Comment on lines +711 to +726

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Determine which IP family findNodeEgressIPsBaremetal returns on dual-stack clusters.
ast-grep run --pattern 'func findNodeEgressIPsBaremetal($$$) { $$$ }' --lang go test/extended/networking/egressip_helpers.go

Repository: openshift/origin

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)egressip(_helpers)?\.go$|test/extended/(include\.go)$' || true

echo "== egressip.go outline =="
ast-grep outline test/extended/networking/egressip.go --match 'egress' --view expanded || true

echo "== helpers outline =="
if [ -f test/extended/networking/egressip_helpers.go ]; then
  ast-grep outline test/extended/networking/egressip_helpers.go --view expanded || true
fi

echo "== relevant searches =="
rg -n "findNodeEgressIPsBaremetal|isIPv6|egressIPStr|hasIPv|baremetal|dual-stack|dual stack|dualstack" test/extended/networking/egressip.go test/extended/networking/egressip_helpers.go 2>/dev/null || true

Repository: openshift/origin

Length of output: 11727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test context 620-810 =="
sed -n '620,810p' test/extended/networking/egressip.go

echo "== findNodeEgressIPs 882-940 =="
sed -n '882,940p' test/extended/networking/egressip_helpers.go

echo "== baremetal allocator 1834-1940 =="
sed -n '1834,1945p' test/extended/networking/egressip_helpers.go

echo "== checkForDuplicateMACOnNode 1767-1815 =="
sed -n '1767,1815p' test/extended/networking/egressip_helpers.go

echo "== behavioral probe: current dual-stack selector and address-family selection =="
python3 - <<'PY'
IP_STR = "fd12:3456:7890:abcd::1"
for is_ipv6 in [False, True]:
    import subprocess
    cmd = "arping -c 1 -I eth0 fd12:3456:7890:abcd::1 2>&1" if not is_ipv6 else "ndisc6 -1 -w 1000 fd12:3456:7890:abcd::1 eth0 2>&1"
    res = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    print(is_ipv6, res.returncode, res.stdout.splitlines()[0] if res.stdout else "")
PY

Repository: openshift/origin

Length of output: 16004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getFirstFreeIPs 982-1052 =="
sed -n '982,1052p' test/extended/networking/egressip_helpers.go

echo "== getNodeEgressIPConfiguration 1053-1070 =="
sed -n '1053,1070p' test/extended/networking/egressip_helpers.go

echo "== GetIPAddressFamily occurrences =="
rg -n "func GetIPAddressFamily|GetIPAddressFamily\\(" -S .

echo "== deterministic family selection behavior from source comments =="
python3 - <<'PY'
from pathlib import Path
p = Path('test/extended/networking/egressip.go')
text = p.read_text()
for key in ['hasIPv4, hasIPv6, err = GetIPAddressFamily(oc)', 'getFirstFreeIPs']:
    print(key, text.find(key))
PY

Repository: openshift/origin

Length of output: 4921


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== util GetIPAddressFamily 550-630 =="
sed -n '550,630p' test/extended/networking/util.go

echo "== check duplicate family source around line 1486 util.go =="
sed -n '1470,1505p' test/extended/networking/util.go

echo "== getTargetProtocolHostPort 1630-1670 =="
sed -n '1630,1670p' test/extended/networking/egressip_helpers.go

Repository: openshift/origin

Length of output: 3969


🌐 Web query:

OVN-Kubernetes node egress-ipconfig annotation IPv6IPv4 dual-stack EgressIP allocation findNodeEgressIPsBaremetal

💡 Result:

In OVN-Kubernetes, the cloud.network.openshift.io/egress-ipconfig annotation is used to provide metadata about a node's networking capabilities—specifically interface information, subnets (ifaddr), and IP address capacity—in public cloud environments (AWS, Azure, Google Cloud) [1][2][3][4]. This annotation allows the EgressIP controller to understand how many egress IPs can be assigned to a node and on which interface [1][3][4]. Regarding IPv4/IPv6 dual-stack and EgressIP allocation: 1. Dual-Stack Configuration: For clusters configured for dual-stack networking, a single EgressIP object can support both IPv4 and IPv6 addresses [1][2][3][5]. Red Hat does not support using two separate EgressIP objects to achieve dual-stack functionality (e.g., one for IPv4 and one for IPv6) [1][2][3][6]. 2. Egress IP Allocation: Egress IPs are assigned to nodes that have been labeled with k8s.ovn.org/egress-assignable="" [7][8][9][5]. The OVN-Kubernetes controller tracks these assignments [10][11]. While the term findNodeEgressIPsBaremetal is not a standard documented API or command, the assignment process involves the EgressIP controller (running in ovnkube-cluster-manager) periodically checking for reachable nodes with the required label to host the assigned egress IPs [8]. If a node is no longer reachable, the controller revalidates and moves the egress IPs to another usable node [8]. 3. Platform Support: While the cloud.network.openshift.io/egress-ipconfig annotation is specific to public cloud environments, the EgressIP feature is also supported on bare-metal clusters [9]. On bare-metal, the requirement to label nodes with k8s.ovn.org/egress-assignable="" remains the primary mechanism for directing where egress traffic is handled [7][9].

Citations:


Select the discovery command from the allocated EgressIP family.

isIPv6 is true only for IPv6-only clusters, while findNodeEgressIPsBaremetal still tries IPv6 first when no IPv4 range is configured. On a dual-stack cluster, an allocated IPv6 egressIPStr then follows the arping path and can fail. Use net.ParseIP(egressIPStr).To4() == nil for the discovery and duplicate-MAC checks instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 711 - 726, Replace the
isIPv6-based branching in the discovery and duplicate-MAC validation flow with a
family check derived from net.ParseIP(egressIPStr).To4() == nil. Use that result
to select the IPv6 ndisc6 command and regex for IPv6 allocated addresses, while
preserving the arping path for IPv4 addresses, including dual-stack clusters.


g.By("9. Getting ovnkube-node pod name on egress node 1")
egressNode1PodInfo, err := ovnkubePod(oc, egressNode1Name)
o.Expect(err).NotTo(o.HaveOccurred())
framework.Logf("Found ovnkube-node pod: %s on node %s", egressNode1PodInfo.podName, egressNode1Name)

g.By("10. Starting goroutine to monitor nftables chain creation during pod deletion")
nftChainFound := make(chan bool, 1)
stopChecking := make(chan bool, 1)
goroutineReady := make(chan bool, 1)
nftChainCheckCmd := "nft list chains 2>/dev/null | grep -q egressip-drop && echo FOUND || echo NOTFOUND"
go func() {
defer close(nftChainFound)
goroutineReady <- true
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stopChecking:
return
case <-ticker.C:
// Use oc debug to run on the node directly since the ovnkube-node pod may be terminating
result, debugErr := oc.AsAdmin().Run("debug").Args(
"node/"+egressNode1Name,
"--",
"chroot", "/host",
"/bin/bash", "-c",
nftChainCheckCmd,
).Output()
if debugErr == nil && strings.Contains(result, "FOUND") {
nftChainFound <- true
return
}
}
}
}()
<-goroutineReady
framework.Logf("Nftables chain monitoring goroutine started")
Comment on lines +733 to +764

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect exutil.CLI for mutable state that makes concurrent use unsafe.
fd -t f 'cli.go' test/extended/util | xargs -r rg -nP -C3 '(func \(c \*CLI\) (AsAdmin|Run|Args|WithoutNamespace)|type CLI struct)'

Repository: openshift/origin

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate egressip.go and util files =="
fd -t f 'egressip.go|cli.go' test | sed -n '1,120p'

echo
echo "== locate CLI type/AsAdmin/Run/Args definitions =="
rg -n "type CLI struct|func \\(c \\*CLI\\) (AsAdmin|Run|Args|WithoutNamespace|SetNamespace)" test -S

Repository: openshift/origin

Length of output: 720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== read egressip.go around goroutine and later oc/clientset usage =="
sed -n '700,810p' test/extended/networking/egressip.go

echo
echo "== inspect relevant CLI implementation files =="
for f in $(rg -l "type CLI struct|func \\(c \\*CLI\\) Run" test -S); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: openshift/origin

Length of output: 13052


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runOcc/Output/AsAdmin/SetNamespace implementation =="
sed -n '240,320p' test/extended/util/client.go
sed -n '860,1025p' test/extended/util/client.go

echo
echo "== occurrences of runOcWithRetry/Output usages in surrounding egressip.go =="
rg -n "runOcWithRetry|oc\\.Output\\(|oc\\.SetNamespace|\\.Output\\(" test/extended/networking/egressip.go test/extended/util/client.go

Repository: openshift/origin

Length of output: 9021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== client.go outputs/runOcc implementation =="
sed -n '970,1055p' test/extended/util/client.go

echo
echo "== helper definitions in egressip.go =="
sed -n '150,205p' test/extended/networking/egressip.go

echo
echo "== read-only structural check: goroutine call chain and later runOcWithRetry/Output calls =="
python3 - <<'PY'
from pathlib import Path
p = Path('test/extended/networking/egressip.go')
s = p.read_text()
lines = s.splitlines()
calls = []
for i,l in enumerate(lines,1):
    if 'go func()' in l or 'oc.AsAdmin().Run("debug")' in l or 'oc.AsAdmin()' in l:
        calls.append((i,l.strip()))
print('\n'.join(f'{i}:{line}' for i,line in calls))
PY

Repository: openshift/origin

Length of output: 6750


Do not share one CLI instance between the monitor goroutine and the main test path.

The goroutine and later test helpers both derive commands from oc, while CLI#Args and CLI#start mutate command state. Concurrent runOcWithRetry(...) calls and .Output() from the monitor can mix argument/execution state. Build a separate CLI for the monitor, or keep the monitor out of concurrent CLI invocations. Also, increase the polling interval from 200ms; each oc debug launch is too heavy to complete in that window, so the loop queues work and adds cluster load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 733 - 764, Update the
nftables monitoring goroutine around nftChainCheckCmd to use a separate CLI
instance from the main test path, preventing concurrent mutation of shared oc
command state during Output execution. Also increase the ticker interval beyond
200ms to allow each oc debug invocation to complete without queuing repeated
commands and adding unnecessary cluster load.


g.By("11. Deleting ovnkube-node pod on egress node 1 to trigger nftables rules and EgressIP migration")
framework.Logf("Deleting ovnkube-node pod %s to trigger EgressIP migration", egressNode1PodInfo.podName)
err = clientset.CoreV1().Pods("openshift-ovn-kubernetes").Delete(context.TODO(), egressNode1PodInfo.podName, metav1.DeleteOptions{})
o.Expect(err).NotTo(o.HaveOccurred())

g.By("12. Verifying nftables chain egressip-drop exists on egress node 1 during shutdown")
select {
case found := <-nftChainFound:
o.Expect(found).To(o.BeTrue(), "nftables chain egressip-drop should be found on egress node 1")
framework.Logf("Nftables chain egressip-drop verified on node %s", egressNode1Name)
case <-time.After(60 * time.Second):
close(stopChecking)
framework.Failf("Timed out waiting for nftables chain egressip-drop on node %s", egressNode1Name)
}
close(stopChecking)
Comment on lines +771 to +780

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid the double close of stopChecking.

The timeout branch closes stopChecking at line 777 and line 780 closes it again. Today framework.Failf panics through Ginkgo, so line 780 is not reached. The code depends on that panic behavior. If the failure path ever returns, the second close panics with "close of closed channel".

Close the channel once with a defer placed right after the goroutine starts.

🐛 Proposed change
 		<-goroutineReady
+		defer close(stopChecking)
 		framework.Logf("Nftables chain monitoring goroutine started")
@@
 		case <-time.After(60 * time.Second):
-			close(stopChecking)
 			framework.Failf("Timed out waiting for nftables chain egressip-drop on node %s", egressNode1Name)
 		}
-		close(stopChecking)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 771 - 780, Update the
shutdown verification around the nftChainFound select to close stopChecking
exactly once: remove the timeout-branch close and register a defer immediately
after starting the checker goroutine to close the channel. Preserve the existing
timeout failure behavior and successful verification flow.


g.By("13. Waiting for EgressIP to migrate to egress node 2")
o.Eventually(func() bool {
hasIP, assignedNode, err = egressIPStatusHasIP(oc, egressIPObjectName, egressIPStr)
if err != nil {
framework.Logf("Error checking EgressIP status: %v", err)
return false
}
if hasIP && assignedNode == egressNode2Name {
return true
}
framework.Logf("EgressIP %s still on node %s, waiting for migration to %s", egressIPStr, assignedNode, egressNode2Name)
return false
}, 120*time.Second, 5*time.Second).Should(o.BeTrue(),
fmt.Sprintf("EgressIP %s should migrate to node %s", egressIPStr, egressNode2Name))
framework.Logf("EgressIP successfully migrated to node %s", egressNode2Name)

g.By("14. Checking for duplicate MAC responses after migration")
err = checkForDuplicateMACOnNode(
oc,
probeNodeName,
physInterface,
egressIPStr,
mac1,
mac2,
isIPv6,
20,
500*time.Millisecond,
)
o.Expect(err).NotTo(o.HaveOccurred(), "duplicate MAC detection check failed")

g.By("15. Waiting for ovnkube-node pod to restart on egress node 1")
o.Eventually(func() bool {
pods, listErr := clientset.CoreV1().Pods("openshift-ovn-kubernetes").List(context.TODO(), metav1.ListOptions{
FieldSelector: fmt.Sprintf("spec.nodeName=%s", egressNode1Name),
LabelSelector: "app=ovnkube-node",
})
if listErr != nil {
return false
}
for _, p := range pods.Items {
if p.Status.Phase == corev1.PodRunning && p.DeletionTimestamp == nil {
for _, c := range p.Status.ContainerStatuses {
if c.Ready {
return true
}
}
}
}
return false
}, 120*time.Second, 5*time.Second).Should(o.BeTrue(),
"ovnkube-node pod should restart on egress node 1")
framework.Logf("ovnkube-node pod restarted on node %s", egressNode1Name)

g.By("16. Verifying nftables cleanup on egress node 1 after pod restart")
newPodInfo, err := ovnkubePod(oc, egressNode1Name)
o.Expect(err).NotTo(o.HaveOccurred())
nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1"
nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
if nftErr == nil {
o.Expect(nftOutput).To(o.Or(
o.ContainSubstring("No such file or directory"),
o.ContainSubstring("No such file"),
), "nftables egress IP table should be deleted after cleanup")
}
framework.Logf("Nftables table cleaned up on node %s", egressNode1Name)
Comment on lines +838 to +846

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The nftables cleanup check passes when the command fails.

nft list table netdev ovn-kubernetes-egressip exits non-zero when the table is absent. adminExecInPod then returns an error, so nftErr != nil and the assertion at lines 841-844 is skipped. Line 846 still logs success. The cleanup verification therefore passes without checking anything, both when the table is gone and when the exec fails for an unrelated reason.

Assert on the output in both cases, and treat an unexpected error as a failure. The two ContainSubstring matchers also overlap; keep only "No such file".

🐛 Proposed change
-		nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1"
-		nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
-		if nftErr == nil {
-			o.Expect(nftOutput).To(o.Or(
-				o.ContainSubstring("No such file or directory"),
-				o.ContainSubstring("No such file"),
-			), "nftables egress IP table should be deleted after cleanup")
-		}
+		// `nft list table` exits non-zero when the table is absent, so ignore the exit
+		// status and assert on the combined output instead.
+		nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1 || true"
+		nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
+		o.Expect(nftErr).NotTo(o.HaveOccurred())
+		o.Expect(nftOutput).To(o.ContainSubstring("No such file"),
+			"nftables egress IP table should be deleted after cleanup")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1"
nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
if nftErr == nil {
o.Expect(nftOutput).To(o.Or(
o.ContainSubstring("No such file or directory"),
o.ContainSubstring("No such file"),
), "nftables egress IP table should be deleted after cleanup")
}
framework.Logf("Nftables table cleaned up on node %s", egressNode1Name)
// `nft list table` exits non-zero when the table is absent, so ignore the exit
// status and assert on the combined output instead.
nftCheckCmd := "nft list table netdev ovn-kubernetes-egressip 2>&1 || true"
nftOutput, nftErr := adminExecInPod(oc, "openshift-ovn-kubernetes", newPodInfo.podName, newPodInfo.containerName, nftCheckCmd)
o.Expect(nftErr).NotTo(o.HaveOccurred())
o.Expect(nftOutput).To(o.ContainSubstring("No such file"),
"nftables egress IP table should be deleted after cleanup")
framework.Logf("Nftables table cleaned up on node %s", egressNode1Name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/networking/egressip.go` around lines 838 - 846, Update the
nftables cleanup verification around adminExecInPod so it always validates the
command output, including when the command returns a non-zero error because the
table is absent. Retain only the “No such file” matcher, and fail explicitly for
unexpected adminExecInPod errors instead of logging success unconditionally.


framework.Logf("Test passed: EgressIP migrated cleanly without duplicate MAC responses")
})
})

//
// Functions to reduce code duplication below - those could also go into egressip_helpers.go, but they feel more appropriate here as they call
// the various testing framework matchers such as o.Expect, etc. These functions also have no return value.
Expand Down
Loading