Skip to content

Commit 80ca2cf

Browse files
alebedev87claude
andcommitted
NE-2422: Add Classic LB test and refactor dual-stack ingress tests
Add a second test case that verifies IPv4 connectivity through a Classic LB ingress controller on a dual-stack cluster. Extract common setup logic (backend service/pod creation, edge route creation, route admission waiting) into shared helper functions. Use distinct domains (nlb/clb) for each test's ingress controller shard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bbeb835 commit 80ca2cf

1 file changed

Lines changed: 192 additions & 122 deletions

File tree

test/extended/router/dualstack.go

Lines changed: 192 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,15 @@ var _ = g.Describe("[sig-network-edge][OCPFeatureGate:AWSDualStackInstall][Featu
3737
ctx := context.Background()
3838

3939
g.By("Checking that the Infrastructure CR has a DualStack IPFamily")
40-
infra, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(ctx, "cluster", metav1.GetOptions{})
41-
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get infrastructure CR")
42-
43-
if infra.Status.PlatformStatus == nil || infra.Status.PlatformStatus.Type != configv1.AWSPlatformType {
44-
g.Skip("Test requires AWS platform")
45-
}
46-
if infra.Status.PlatformStatus.AWS == nil {
47-
g.Skip("AWS platform status is not set")
48-
}
49-
ipFamily := infra.Status.PlatformStatus.AWS.IPFamily
50-
if ipFamily != configv1.DualStackIPv4Primary && ipFamily != configv1.DualStackIPv6Primary {
51-
g.Skip(fmt.Sprintf("Test requires DualStack IPFamily, got %q", ipFamily))
52-
}
40+
requireAWSDualStack(ctx, oc)
5341

5442
g.By("Getting the default ingress domain")
5543
defaultDomain, err := getDefaultIngressClusterDomainName(oc, time.Minute)
5644
o.Expect(err).NotTo(o.HaveOccurred(), "failed to find default domain name")
5745

5846
ns := oc.KubeFramework().Namespace.Name
5947
baseDomain := strings.TrimPrefix(defaultDomain, "apps.")
60-
shardFQDN := "hosts." + baseDomain
48+
shardFQDN := "nlb." + baseDomain
6149

6250
// Deploy the shard first so DNS and LB can provision while we set up the backend.
6351
g.By("Deploying a new router shard with NLB")
@@ -87,137 +75,219 @@ var _ = g.Describe("[sig-network-edge][OCPFeatureGate:AWSDualStackInstall][Featu
8775
err = oc.AsAdmin().Run("label").Args("namespace", oc.Namespace(), "type="+oc.Namespace()).Execute()
8876
o.Expect(err).NotTo(o.HaveOccurred())
8977

90-
g.By("Creating backend service")
91-
service := &corev1.Service{
92-
ObjectMeta: metav1.ObjectMeta{
93-
Name: "dualstack-backend",
94-
Labels: map[string]string{
95-
"app": "dualstack-backend",
96-
},
97-
},
98-
Spec: corev1.ServiceSpec{
99-
Selector: map[string]string{
100-
"app": "dualstack-backend",
101-
},
102-
IPFamilyPolicy: func() *corev1.IPFamilyPolicy {
103-
p := corev1.IPFamilyPolicyPreferDualStack
104-
return &p
105-
}(),
106-
Ports: []corev1.ServicePort{
107-
{
108-
Name: "http",
109-
Port: 8080,
110-
Protocol: corev1.ProtocolTCP,
111-
TargetPort: intstr.FromInt(8080),
78+
g.By("Creating backend service and pod")
79+
createBackendServiceAndPod(ctx, oc, ns, "dualstack-backend")
80+
81+
g.By("Creating an edge-terminated route")
82+
routeHost := "dualstack-test." + shardFQDN
83+
createEdgeRoute(ctx, oc, ns, "dualstack-route", routeHost, "dualstack-backend")
84+
85+
g.By("Waiting for the route to be admitted")
86+
waitForRouteAdmitted(ctx, oc, ns, "dualstack-route", routeHost)
87+
88+
g.By("Creating exec pod for curl tests")
89+
execPod := exutil.CreateExecPodOrFail(oc.AdminKubeClient(), ns, "execpod")
90+
defer func() {
91+
oc.AdminKubeClient().CoreV1().Pods(ns).Delete(ctx, execPod.Name, *metav1.NewDeleteOptions(1))
92+
}()
93+
94+
g.By("Verifying route is reachable over IPv4")
95+
err = waitForRouteResponse(ns, execPod.Name, routeHost, "-4", 10*time.Minute)
96+
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv4")
97+
98+
g.By("Verifying route is reachable over IPv6")
99+
err = waitForRouteResponse(ns, execPod.Name, routeHost, "-6", 10*time.Minute)
100+
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv6")
101+
})
102+
103+
g.It("should be reachable via IPv4 through a Classic LB ingress controller on a dual-stack cluster", func() {
104+
ctx := context.Background()
105+
106+
g.By("Checking that the Infrastructure CR has a DualStack IPFamily")
107+
requireAWSDualStack(ctx, oc)
108+
109+
g.By("Getting the default ingress domain")
110+
defaultDomain, err := getDefaultIngressClusterDomainName(oc, time.Minute)
111+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to find default domain name")
112+
113+
ns := oc.KubeFramework().Namespace.Name
114+
baseDomain := strings.TrimPrefix(defaultDomain, "apps.")
115+
shardFQDN := "clb." + baseDomain
116+
117+
// Deploy the shard first so DNS and LB can provision while we set up the backend.
118+
g.By("Deploying a new router shard with Classic LB")
119+
shardIngressCtrl, err := shard.DeployNewRouterShard(oc, 10*time.Minute, shard.Config{
120+
Domain: shardFQDN,
121+
Type: oc.Namespace(),
122+
LoadBalancer: &operatorv1.LoadBalancerStrategy{
123+
Scope: operatorv1.ExternalLoadBalancer,
124+
ProviderParameters: &operatorv1.ProviderLoadBalancerParameters{
125+
Type: operatorv1.AWSLoadBalancerProvider,
126+
AWS: &operatorv1.AWSLoadBalancerParameters{
127+
Type: operatorv1.AWSClassicLoadBalancer,
112128
},
113129
},
114130
},
115-
}
116-
_, err = oc.AdminKubeClient().CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
131+
})
132+
defer func() {
133+
if shardIngressCtrl != nil {
134+
if err := oc.AdminOperatorClient().OperatorV1().IngressControllers(shardIngressCtrl.Namespace).Delete(ctx, shardIngressCtrl.Name, metav1.DeleteOptions{}); err != nil {
135+
e2e.Logf("deleting ingress controller failed: %v\n", err)
136+
}
137+
}
138+
}()
139+
o.Expect(err).NotTo(o.HaveOccurred(), "new router shard did not rollout")
140+
141+
g.By("Labelling the namespace for the shard")
142+
err = oc.AsAdmin().Run("label").Args("namespace", oc.Namespace(), "type="+oc.Namespace()).Execute()
117143
o.Expect(err).NotTo(o.HaveOccurred())
118144

119-
g.By("Creating backend pod")
120-
backendPod := &corev1.Pod{
121-
ObjectMeta: metav1.ObjectMeta{
122-
Name: "dualstack-backend",
123-
Labels: map[string]string{
124-
"app": "dualstack-backend",
145+
g.By("Creating backend service and pod")
146+
createBackendServiceAndPod(ctx, oc, ns, "classic-backend")
147+
148+
g.By("Creating an edge-terminated route")
149+
routeHost := "classic-test." + shardFQDN
150+
createEdgeRoute(ctx, oc, ns, "classic-route", routeHost, "classic-backend")
151+
152+
g.By("Waiting for the route to be admitted")
153+
waitForRouteAdmitted(ctx, oc, ns, "classic-route", routeHost)
154+
155+
g.By("Creating exec pod for curl tests")
156+
execPod := exutil.CreateExecPodOrFail(oc.AdminKubeClient(), ns, "execpod")
157+
defer func() {
158+
oc.AdminKubeClient().CoreV1().Pods(ns).Delete(ctx, execPod.Name, *metav1.NewDeleteOptions(1))
159+
}()
160+
161+
g.By("Verifying route is reachable over IPv4")
162+
err = waitForRouteResponse(ns, execPod.Name, routeHost, "-4", 10*time.Minute)
163+
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv4")
164+
})
165+
})
166+
167+
func requireAWSDualStack(ctx context.Context, oc *exutil.CLI) {
168+
infra, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(ctx, "cluster", metav1.GetOptions{})
169+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get infrastructure CR")
170+
171+
if infra.Status.PlatformStatus == nil || infra.Status.PlatformStatus.Type != configv1.AWSPlatformType {
172+
g.Skip("Test requires AWS platform")
173+
}
174+
if infra.Status.PlatformStatus.AWS == nil {
175+
g.Skip("AWS platform status is not set")
176+
}
177+
ipFamily := infra.Status.PlatformStatus.AWS.IPFamily
178+
if ipFamily != configv1.DualStackIPv4Primary && ipFamily != configv1.DualStackIPv6Primary {
179+
g.Skip(fmt.Sprintf("Test requires DualStack IPFamily, got %q", ipFamily))
180+
}
181+
}
182+
183+
func createBackendServiceAndPod(ctx context.Context, oc *exutil.CLI, ns, name string) {
184+
service := &corev1.Service{
185+
ObjectMeta: metav1.ObjectMeta{
186+
Name: name,
187+
Labels: map[string]string{"app": name},
188+
},
189+
Spec: corev1.ServiceSpec{
190+
Selector: map[string]string{"app": name},
191+
IPFamilyPolicy: func() *corev1.IPFamilyPolicy {
192+
p := corev1.IPFamilyPolicyPreferDualStack
193+
return &p
194+
}(),
195+
Ports: []corev1.ServicePort{
196+
{
197+
Name: "http",
198+
Port: 8080,
199+
Protocol: corev1.ProtocolTCP,
200+
TargetPort: intstr.FromInt(8080),
125201
},
126202
},
127-
Spec: corev1.PodSpec{
128-
TerminationGracePeriodSeconds: utilpointer.Int64(1),
129-
Containers: []corev1.Container{
130-
{
131-
Name: "server",
132-
Image: image.ShellImage(),
133-
ImagePullPolicy: corev1.PullIfNotPresent,
134-
Command: []string{"/bin/bash", "-c", `while true; do
203+
},
204+
}
205+
_, err := oc.AdminKubeClient().CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
206+
o.Expect(err).NotTo(o.HaveOccurred())
207+
208+
pod := &corev1.Pod{
209+
ObjectMeta: metav1.ObjectMeta{
210+
Name: name,
211+
Labels: map[string]string{"app": name},
212+
},
213+
Spec: corev1.PodSpec{
214+
TerminationGracePeriodSeconds: utilpointer.Int64(1),
215+
Containers: []corev1.Container{
216+
{
217+
Name: "server",
218+
Image: image.ShellImage(),
219+
ImagePullPolicy: corev1.PullIfNotPresent,
220+
Command: []string{"/bin/bash", "-c", `while true; do
135221
printf "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/plain\r\n\r\nOK" | ncat -l 8080 --send-only || true
136222
done`},
137-
Ports: []corev1.ContainerPort{
138-
{
139-
ContainerPort: 8080,
140-
Name: "http",
141-
Protocol: corev1.ProtocolTCP,
142-
},
223+
Ports: []corev1.ContainerPort{
224+
{
225+
ContainerPort: 8080,
226+
Name: "http",
227+
Protocol: corev1.ProtocolTCP,
143228
},
144229
},
145230
},
146231
},
147-
}
148-
_, err = oc.AdminKubeClient().CoreV1().Pods(ns).Create(ctx, backendPod, metav1.CreateOptions{})
149-
o.Expect(err).NotTo(o.HaveOccurred())
232+
},
233+
}
234+
_, err = oc.AdminKubeClient().CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{})
235+
o.Expect(err).NotTo(o.HaveOccurred())
150236

151-
g.By("Waiting for backend pod to be running")
152-
e2e.ExpectNoError(e2epod.WaitForPodRunningInNamespaceSlow(ctx, oc.KubeClient(), "dualstack-backend", ns), "backend pod not running")
237+
e2e.ExpectNoError(e2epod.WaitForPodRunningInNamespaceSlow(ctx, oc.KubeClient(), name, ns), "backend pod not running")
238+
}
153239

154-
g.By("Creating an edge-terminated route")
155-
routeType := oc.Namespace()
156-
route := routev1.Route{
157-
ObjectMeta: metav1.ObjectMeta{
158-
Name: "dualstack-route",
159-
Labels: map[string]string{
160-
"type": routeType,
161-
},
240+
func createEdgeRoute(ctx context.Context, oc *exutil.CLI, ns, name, host, serviceName string) {
241+
route := routev1.Route{
242+
ObjectMeta: metav1.ObjectMeta{
243+
Name: name,
244+
Labels: map[string]string{
245+
"type": oc.Namespace(),
162246
},
163-
Spec: routev1.RouteSpec{
164-
Host: "dualstack-test." + shardFQDN,
165-
Port: &routev1.RoutePort{
166-
TargetPort: intstr.FromInt(8080),
167-
},
168-
TLS: &routev1.TLSConfig{
169-
Termination: routev1.TLSTerminationEdge,
170-
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
171-
},
172-
To: routev1.RouteTargetReference{
173-
Kind: "Service",
174-
Name: "dualstack-backend",
175-
Weight: utilpointer.Int32(100),
176-
},
177-
WildcardPolicy: routev1.WildcardPolicyNone,
247+
},
248+
Spec: routev1.RouteSpec{
249+
Host: host,
250+
Port: &routev1.RoutePort{
251+
TargetPort: intstr.FromInt(8080),
178252
},
179-
}
180-
_, err = oc.RouteClient().RouteV1().Routes(ns).Create(ctx, &route, metav1.CreateOptions{})
181-
o.Expect(err).NotTo(o.HaveOccurred())
253+
TLS: &routev1.TLSConfig{
254+
Termination: routev1.TLSTerminationEdge,
255+
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
256+
},
257+
To: routev1.RouteTargetReference{
258+
Kind: "Service",
259+
Name: serviceName,
260+
Weight: utilpointer.Int32(100),
261+
},
262+
WildcardPolicy: routev1.WildcardPolicyNone,
263+
},
264+
}
265+
_, err := oc.RouteClient().RouteV1().Routes(ns).Create(ctx, &route, metav1.CreateOptions{})
266+
o.Expect(err).NotTo(o.HaveOccurred())
267+
}
182268

183-
g.By("Waiting for the route to be admitted")
184-
routeHost := "dualstack-test." + shardFQDN
185-
err = wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) {
186-
r, err := oc.RouteClient().RouteV1().Routes(ns).Get(ctx, "dualstack-route", metav1.GetOptions{})
187-
if err != nil {
188-
e2e.Logf("failed to get route: %v, retrying...", err)
189-
return false, nil
190-
}
191-
for _, ingress := range r.Status.Ingress {
192-
if ingress.Host == routeHost {
193-
for _, condition := range ingress.Conditions {
194-
if condition.Type == routev1.RouteAdmitted && condition.Status == corev1.ConditionTrue {
195-
return true, nil
196-
}
269+
func waitForRouteAdmitted(ctx context.Context, oc *exutil.CLI, ns, name, host string) {
270+
err := wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) {
271+
r, err := oc.RouteClient().RouteV1().Routes(ns).Get(ctx, name, metav1.GetOptions{})
272+
if err != nil {
273+
e2e.Logf("failed to get route: %v, retrying...", err)
274+
return false, nil
275+
}
276+
for _, ingress := range r.Status.Ingress {
277+
if ingress.Host == host {
278+
for _, condition := range ingress.Conditions {
279+
if condition.Type == routev1.RouteAdmitted && condition.Status == corev1.ConditionTrue {
280+
return true, nil
197281
}
198282
}
199283
}
200-
return false, nil
201-
})
202-
o.Expect(err).NotTo(o.HaveOccurred(), "route was not admitted")
203-
204-
g.By("Creating exec pod for curl tests")
205-
execPod := exutil.CreateExecPodOrFail(oc.AdminKubeClient(), ns, "execpod")
206-
defer func() {
207-
oc.AdminKubeClient().CoreV1().Pods(ns).Delete(ctx, execPod.Name, *metav1.NewDeleteOptions(1))
208-
}()
209-
210-
g.By("Verifying route is reachable over IPv4")
211-
err = waitForDualStackRouteResponse(ns, execPod.Name, routeHost, "-4", 10*time.Minute)
212-
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv4")
213-
214-
g.By("Verifying route is reachable over IPv6")
215-
err = waitForDualStackRouteResponse(ns, execPod.Name, routeHost, "-6", 10*time.Minute)
216-
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv6")
284+
}
285+
return false, nil
217286
})
218-
})
287+
o.Expect(err).NotTo(o.HaveOccurred(), "route was not admitted")
288+
}
219289

220-
func waitForDualStackRouteResponse(ns, execPodName, host, ipFlag string, timeout time.Duration) error {
290+
func waitForRouteResponse(ns, execPodName, host, ipFlag string, timeout time.Duration) error {
221291
curlCmd := fmt.Sprintf("curl %s -k -v -m 10 --connect-timeout 5 -o /dev/null https://%s 2>&1", ipFlag, host)
222292
var lastOutput string
223293
err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) {

0 commit comments

Comments
 (0)