Skip to content

Commit 734f097

Browse files
alebedev87claude
andcommitted
NE-2422: Add dual-stack ingress e2e test for AWSDualStackInstall featuregate
Add a new test that verifies ingress is reachable over both IPv4 and IPv6 on AWS dual-stack clusters. The test creates a new IngressController shard with an NLB, an edge-terminated route, and validates connectivity using curl -4 and curl -6 from an exec pod. Also extends the shard.Config to support optional LoadBalancerStrategy parameters for configuring the IngressController's load balancer type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2fa0e14 commit 734f097

2 files changed

Lines changed: 247 additions & 1 deletion

File tree

test/extended/router/dualstack.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package router
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
g "github.com/onsi/ginkgo/v2"
9+
o "github.com/onsi/gomega"
10+
11+
configv1 "github.com/openshift/api/config/v1"
12+
operatorv1 "github.com/openshift/api/operator/v1"
13+
routev1 "github.com/openshift/api/route/v1"
14+
15+
corev1 "k8s.io/api/core/v1"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/util/intstr"
18+
"k8s.io/apimachinery/pkg/util/wait"
19+
e2e "k8s.io/kubernetes/test/e2e/framework"
20+
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
21+
e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
22+
admissionapi "k8s.io/pod-security-admission/api"
23+
utilpointer "k8s.io/utils/pointer"
24+
25+
"github.com/openshift/origin/test/extended/router/shard"
26+
exutil "github.com/openshift/origin/test/extended/util"
27+
"github.com/openshift/origin/test/extended/util/image"
28+
)
29+
30+
var _ = g.Describe("[sig-network-edge][OCPFeatureGate:AWSDualStackInstall][Feature:Router][apigroup:route.openshift.io][apigroup:operator.openshift.io][apigroup:config.openshift.io]", func() {
31+
defer g.GinkgoRecover()
32+
33+
var oc = exutil.NewCLIWithPodSecurityLevel("router-dualstack", admissionapi.LevelBaseline)
34+
35+
g.It("should be reachable via IPv4 and IPv6 through a dual-stack ingress controller", func() {
36+
ctx := context.Background()
37+
38+
g.By("Checking that the Infrastructure CR has a DualStack IPFamily")
39+
infra, err := oc.AdminConfigClient().ConfigV1().Infrastructures().Get(ctx, "cluster", metav1.GetOptions{})
40+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get infrastructure CR")
41+
42+
if infra.Status.PlatformStatus == nil || infra.Status.PlatformStatus.Type != configv1.AWSPlatformType {
43+
g.Skip("Test requires AWS platform")
44+
}
45+
if infra.Status.PlatformStatus.AWS == nil {
46+
g.Skip("AWS platform status is not set")
47+
}
48+
ipFamily := infra.Status.PlatformStatus.AWS.IPFamily
49+
if ipFamily != configv1.DualStackIPv4Primary && ipFamily != configv1.DualStackIPv6Primary {
50+
g.Skip(fmt.Sprintf("Test requires DualStack IPFamily, got %q", ipFamily))
51+
}
52+
53+
g.By("Getting the default ingress domain")
54+
defaultDomain, err := getDefaultIngressClusterDomainName(oc, time.Minute)
55+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to find default domain name")
56+
57+
ns := oc.KubeFramework().Namespace.Name
58+
shardFQDN := oc.Namespace() + "." + defaultDomain
59+
60+
g.By("Creating backend service")
61+
service := &corev1.Service{
62+
ObjectMeta: metav1.ObjectMeta{
63+
Name: "dualstack-backend",
64+
Labels: map[string]string{
65+
"app": "dualstack-backend",
66+
},
67+
},
68+
Spec: corev1.ServiceSpec{
69+
Selector: map[string]string{
70+
"app": "dualstack-backend",
71+
},
72+
IPFamilyPolicy: func() *corev1.IPFamilyPolicy {
73+
p := corev1.IPFamilyPolicyPreferDualStack
74+
return &p
75+
}(),
76+
Ports: []corev1.ServicePort{
77+
{
78+
Name: "http",
79+
Port: 8080,
80+
Protocol: corev1.ProtocolTCP,
81+
TargetPort: intstr.FromInt(8080),
82+
},
83+
},
84+
},
85+
}
86+
_, err = oc.AdminKubeClient().CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
87+
o.Expect(err).NotTo(o.HaveOccurred())
88+
89+
g.By("Creating backend pod")
90+
backendPod := &corev1.Pod{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Name: "dualstack-backend",
93+
Labels: map[string]string{
94+
"app": "dualstack-backend",
95+
},
96+
},
97+
Spec: corev1.PodSpec{
98+
TerminationGracePeriodSeconds: utilpointer.Int64(1),
99+
Containers: []corev1.Container{
100+
{
101+
Name: "server",
102+
Image: image.ShellImage(),
103+
ImagePullPolicy: corev1.PullIfNotPresent,
104+
Command: []string{"/bin/bash", "-c", `while true; do
105+
printf "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/plain\r\n\r\nOK" | nc -l -p 8080 -q 1 || true
106+
done`},
107+
Ports: []corev1.ContainerPort{
108+
{
109+
ContainerPort: 8080,
110+
Name: "http",
111+
Protocol: corev1.ProtocolTCP,
112+
},
113+
},
114+
},
115+
},
116+
},
117+
}
118+
_, err = oc.AdminKubeClient().CoreV1().Pods(ns).Create(ctx, backendPod, metav1.CreateOptions{})
119+
o.Expect(err).NotTo(o.HaveOccurred())
120+
121+
g.By("Waiting for backend pod to be running")
122+
e2e.ExpectNoError(e2epod.WaitForPodRunningInNamespaceSlow(ctx, oc.KubeClient(), "dualstack-backend", ns), "backend pod not running")
123+
124+
g.By("Creating an edge-terminated route")
125+
routeType := oc.Namespace()
126+
route := routev1.Route{
127+
ObjectMeta: metav1.ObjectMeta{
128+
Name: "dualstack-route",
129+
Labels: map[string]string{
130+
"type": routeType,
131+
},
132+
},
133+
Spec: routev1.RouteSpec{
134+
Host: "dualstack-test." + shardFQDN,
135+
Port: &routev1.RoutePort{
136+
TargetPort: intstr.FromInt(8080),
137+
},
138+
TLS: &routev1.TLSConfig{
139+
Termination: routev1.TLSTerminationEdge,
140+
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
141+
},
142+
To: routev1.RouteTargetReference{
143+
Kind: "Service",
144+
Name: "dualstack-backend",
145+
Weight: utilpointer.Int32(100),
146+
},
147+
WildcardPolicy: routev1.WildcardPolicyNone,
148+
},
149+
}
150+
_, err = oc.RouteClient().RouteV1().Routes(ns).Create(ctx, &route, metav1.CreateOptions{})
151+
o.Expect(err).NotTo(o.HaveOccurred())
152+
153+
g.By("Deploying a new router shard with NLB")
154+
shardIngressCtrl, err := shard.DeployNewRouterShard(oc, 10*time.Minute, shard.Config{
155+
Domain: shardFQDN,
156+
Type: oc.Namespace(),
157+
LoadBalancer: &operatorv1.LoadBalancerStrategy{
158+
Scope: operatorv1.ExternalLoadBalancer,
159+
ProviderParameters: &operatorv1.ProviderLoadBalancerParameters{
160+
Type: operatorv1.AWSLoadBalancerProvider,
161+
AWS: &operatorv1.AWSLoadBalancerParameters{
162+
Type: operatorv1.AWSNetworkLoadBalancer,
163+
},
164+
},
165+
},
166+
})
167+
defer func() {
168+
if shardIngressCtrl != nil {
169+
if err := oc.AdminOperatorClient().OperatorV1().IngressControllers(shardIngressCtrl.Namespace).Delete(ctx, shardIngressCtrl.Name, metav1.DeleteOptions{}); err != nil {
170+
e2e.Logf("deleting ingress controller failed: %v\n", err)
171+
}
172+
}
173+
}()
174+
o.Expect(err).NotTo(o.HaveOccurred(), "new router shard did not rollout")
175+
176+
g.By("Labelling the namespace for the shard")
177+
err = oc.AsAdmin().Run("label").Args("namespace", oc.Namespace(), "type="+oc.Namespace()).Execute()
178+
o.Expect(err).NotTo(o.HaveOccurred())
179+
180+
g.By("Waiting for the route to be admitted")
181+
routeHost := "dualstack-test." + shardFQDN
182+
err = wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) {
183+
r, err := oc.RouteClient().RouteV1().Routes(ns).Get(ctx, "dualstack-route", metav1.GetOptions{})
184+
if err != nil {
185+
e2e.Logf("failed to get route: %v, retrying...", err)
186+
return false, nil
187+
}
188+
for _, ingress := range r.Status.Ingress {
189+
if ingress.Host == routeHost {
190+
for _, condition := range ingress.Conditions {
191+
if condition.Type == routev1.RouteAdmitted && condition.Status == corev1.ConditionTrue {
192+
return true, nil
193+
}
194+
}
195+
}
196+
}
197+
return false, nil
198+
})
199+
o.Expect(err).NotTo(o.HaveOccurred(), "route was not admitted")
200+
201+
g.By("Creating exec pod for curl tests")
202+
execPod := exutil.CreateExecPodOrFail(oc.AdminKubeClient(), ns, "execpod")
203+
defer func() {
204+
oc.AdminKubeClient().CoreV1().Pods(ns).Delete(ctx, execPod.Name, *metav1.NewDeleteOptions(1))
205+
}()
206+
207+
g.By("Verifying route is reachable over IPv4")
208+
err = waitForDualStackRouteResponse(ns, execPod.Name, routeHost, "-4", 5*60)
209+
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv4")
210+
211+
g.By("Verifying route is reachable over IPv6")
212+
err = waitForDualStackRouteResponse(ns, execPod.Name, routeHost, "-6", 5*60)
213+
o.Expect(err).NotTo(o.HaveOccurred(), "route not reachable over IPv6")
214+
})
215+
})
216+
217+
func waitForDualStackRouteResponse(ns, execPodName, host, ipFlag string, timeoutSeconds int) error {
218+
cmd := fmt.Sprintf(`
219+
STOP=$(($(date '+%%s') + %d))
220+
while [ $(date '+%%s') -lt $STOP ]; do
221+
rc=0
222+
code=$( curl %s -k -s -m 10 -o /dev/null -w '%%{http_code}' https://%s ) || rc=$?
223+
if [[ "${rc:-0}" -eq 0 ]]; then
224+
echo "response code: $code"
225+
if [[ $code -eq 200 ]]; then
226+
exit 0
227+
fi
228+
else
229+
echo "curl error: ${rc}" 1>&2
230+
fi
231+
sleep 5
232+
done
233+
echo "timed out waiting for 200 response"
234+
exit 1
235+
`, timeoutSeconds, ipFlag, host)
236+
output, err := e2eoutput.RunHostCmd(ns, execPodName, cmd)
237+
if err != nil {
238+
return fmt.Errorf("curl %s to %s failed: %v\n%s", ipFlag, host, err, output)
239+
}
240+
return nil
241+
}

test/extended/router/shard/shard.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ type Config struct {
2121

2222
// Type is the matchSelector
2323
Type string
24+
25+
// LoadBalancer optionally specifies LoadBalancerStrategy parameters.
26+
// If nil, the default LoadBalancer configuration is used.
27+
LoadBalancer *operatorv1.LoadBalancerStrategy
2428
}
2529

2630
var ingressControllerNonDefaultAvailableConditions = []operatorv1.OperatorCondition{
@@ -43,7 +47,8 @@ func DeployNewRouterShard(oc *exutil.CLI, timeout time.Duration, cfg Config) (*o
4347
Replicas: utilpointer.Int32(1),
4448
Domain: cfg.Domain,
4549
EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{
46-
Type: operatorv1.LoadBalancerServiceStrategyType,
50+
Type: operatorv1.LoadBalancerServiceStrategyType,
51+
LoadBalancer: cfg.LoadBalancer,
4752
},
4853
NodePlacement: &operatorv1.NodePlacement{
4954
NodeSelector: &metav1.LabelSelector{

0 commit comments

Comments
 (0)