Skip to content

Commit fdc9e65

Browse files
committed
e2e: enhance test scenarios with NLB
This change enhance test scenarios by: - supporting more distributions which does not allow pods to bind on privileged ports (default behavior of libjig, see issue - refact tests to allow adding more cases - introduce tests to NLB, including advanced tests to validate the node selector annotation. AWS SDK is added to satisfy this validatoin.
1 parent 1c78a7b commit fdc9e65

File tree

1 file changed

+277
-36
lines changed

1 file changed

+277
-36
lines changed

tests/e2e/loadbalancer.go

Lines changed: 277 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,27 @@ limitations under the License.
1414
package e2e
1515

1616
import (
17+
"context"
18+
"fmt"
19+
"strings"
20+
21+
"github.com/onsi/ginkgo/v2"
1722
. "github.com/onsi/ginkgo/v2"
1823
v1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1925
"k8s.io/apimachinery/pkg/util/intstr"
2026
clientset "k8s.io/client-go/kubernetes"
2127
"k8s.io/kubernetes/test/e2e/framework"
2228
e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
29+
imageutils "k8s.io/kubernetes/test/utils/image"
2330
admissionapi "k8s.io/pod-security-admission/api"
31+
32+
"github.com/aws/aws-sdk-go-v2/aws"
33+
"github.com/aws/aws-sdk-go-v2/config"
34+
elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
2435
)
2536

37+
// loadbalancer tests
2638
var _ = Describe("[cloud-provider-aws-e2e] loadbalancer", func() {
2739
f := framework.NewDefaultFramework("cloud-provider-aws")
2840
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged
@@ -41,61 +53,290 @@ var _ = Describe("[cloud-provider-aws-e2e] loadbalancer", func() {
4153
// After each test
4254
})
4355

44-
It("should configure the loadbalancer based on annotations", func() {
45-
loadBalancerCreateTimeout := e2eservice.GetServiceLoadBalancerCreationTimeout(cs)
46-
framework.Logf("Running tests against AWS with timeout %s", loadBalancerCreateTimeout)
56+
type loadBalancerTestCases struct {
57+
Name string
58+
ResourceSuffix string
59+
Annotations map[string]string
60+
PostRunValidations func(cfg *configServiceLB, svc *v1.Service)
61+
}
62+
63+
cases := []loadBalancerTestCases{
64+
{
65+
Name: "should configure the loadbalancer based on annotations",
66+
ResourceSuffix: "",
67+
Annotations: map[string]string{},
68+
},
69+
{
70+
Name: "NLB should configure the loadbalancer based on annotations",
71+
ResourceSuffix: "nlb",
72+
Annotations: map[string]string{
73+
"service.beta.kubernetes.io/aws-load-balancer-type": "nlb",
74+
},
75+
},
76+
{
77+
Name: "NLB should configure the loadbalancer with target-node-labels",
78+
ResourceSuffix: "sg-wk",
79+
Annotations: map[string]string{
80+
"service.beta.kubernetes.io/aws-load-balancer-type": "nlb",
81+
"service.beta.kubernetes.io/aws-load-balancer-target-node-labels": "node-role.kubernetes.io/worker=",
82+
},
83+
PostRunValidations: func(cfg *configServiceLB, svc *v1.Service) {
84+
j := cfg.LBJig
85+
nodeList, err := j.Client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{
86+
LabelSelector: "node-role.kubernetes.io/worker=",
87+
})
88+
framework.ExpectNoError(err, "failed to list worker nodes")
89+
90+
workerNodes := len(nodeList.Items)
91+
framework.Logf("Found %d worker nodes", workerNodes)
92+
93+
// Validate in the TG if the node count matches with expected target-node-labels selector.
94+
lbDNS := svc.Status.LoadBalancer.Ingress[0].Hostname
95+
framework.ExpectNoError(getLBTargetCount(context.TODO(), lbDNS, workerNodes), "AWS LB target count validation failed")
96+
},
97+
},
98+
}
99+
100+
serviceNameBase := "lbconfig-test"
101+
for _, tc := range cases {
102+
It(tc.Name, func() {
103+
loadBalancerCreateTimeout := e2eservice.GetServiceLoadBalancerCreationTimeout(cs)
104+
framework.Logf("Running tests against AWS with timeout %s", loadBalancerCreateTimeout)
105+
106+
// Create Configuration
107+
serviceName := serviceNameBase
108+
if len(tc.ResourceSuffix) > 0 {
109+
serviceName = fmt.Sprintf("%s-%s", serviceName, tc.ResourceSuffix)
110+
}
111+
framework.Logf("namespace for load balancer conig test: %s", ns.Name)
112+
113+
By("creating a TCP service " + serviceName + " with type=LoadBalancerType in namespace " + ns.Name)
114+
lbConfig := newConfigServiceLB()
115+
lbConfig.LBJig = e2eservice.NewTestJig(cs, ns.Name, serviceName)
116+
lbServiceConfig := lbConfig.buildService(tc.Annotations)
117+
118+
// Create Load Balancer
119+
ginkgo.By("creating loadbalancer for service " + lbServiceConfig.Namespace + "/" + lbServiceConfig.Name)
120+
_, err := lbConfig.LBJig.Client.CoreV1().Services(lbConfig.LBJig.Namespace).Create(context.TODO(), lbServiceConfig, metav1.CreateOptions{})
121+
framework.ExpectNoError(fmt.Errorf("failed to create LoadBalancer Service %q: %v", lbServiceConfig.Name, err))
122+
123+
ginkgo.By("waiting for loadbalancer for service " + lbServiceConfig.Namespace + "/" + lbServiceConfig.Name)
124+
lbService, err := lbConfig.LBJig.WaitForLoadBalancer(loadBalancerCreateTimeout)
125+
framework.ExpectNoError(err)
126+
127+
// Run Workloads
128+
By("creating a pod to be part of the TCP service " + serviceName)
129+
_, err = lbConfig.LBJig.Run(lbConfig.buildReplicationController())
130+
framework.ExpectNoError(err)
131+
132+
if tc.PostRunValidations != nil {
133+
By("running post run validations")
134+
tc.PostRunValidations(lbConfig, lbService)
135+
}
136+
137+
// Test the Service Endpoint
138+
By("hitting the TCP service's LB External IP")
139+
svcPort := int(lbService.Spec.Ports[0].Port)
140+
ingressIP := e2eservice.GetIngressPoint(&lbService.Status.LoadBalancer.Ingress[0])
141+
framework.Logf("Load balancer's ingress IP: %s", ingressIP)
142+
143+
e2eservice.TestReachableHTTP(ingressIP, svcPort, e2eservice.LoadBalancerLagTimeoutAWS)
144+
145+
// Update the service to cluster IP
146+
By("changing TCP service back to type=ClusterIP")
147+
_, err = lbConfig.LBJig.UpdateService(func(s *v1.Service) {
148+
s.Spec.Type = v1.ServiceTypeClusterIP
149+
})
150+
framework.ExpectNoError(err)
151+
152+
// Wait for the load balancer to be destroyed asynchronously
153+
_, err = lbConfig.LBJig.WaitForLoadBalancerDestroy(ingressIP, svcPort, loadBalancerCreateTimeout)
154+
framework.ExpectNoError(err)
155+
})
156+
}
157+
})
47158

48-
serviceName := "lbconfig-test"
49-
framework.Logf("namespace for load balancer conig test: %s", ns.Name)
159+
// configServiceLB hold loadbalancer test configurations
160+
type configServiceLB struct {
161+
PodPort uint16
162+
PodProtocol v1.Protocol
163+
DefaultAnnotations map[string]string
50164

51-
By("creating a TCP service " + serviceName + " with type=LoadBalancerType in namespace " + ns.Name)
52-
lbJig := e2eservice.NewTestJig(cs, ns.Name, serviceName)
165+
LBJig *e2eservice.TestJig
166+
}
53167

54-
serviceUpdateFunc := func(svc *v1.Service) {
55-
annotations := make(map[string]string)
56-
annotations["aws-load-balancer-backend-protocol"] = "http"
57-
annotations["aws-load-balancer-ssl-ports"] = "https"
168+
func newConfigServiceLB() *configServiceLB {
169+
return &configServiceLB{
170+
PodPort: 8080,
171+
PodProtocol: v1.ProtocolTCP,
172+
DefaultAnnotations: map[string]string{
173+
"aws-load-balancer-backend-protocol": "http",
174+
"aws-load-balancer-ssl-ports": "https",
175+
},
176+
}
177+
}
58178

59-
svc.Annotations = annotations
60-
svc.Spec.Ports = []v1.ServicePort{
179+
func (s *configServiceLB) buildService(extraAnnotations map[string]string) *v1.Service {
180+
svc := &v1.Service{
181+
ObjectMeta: metav1.ObjectMeta{
182+
Namespace: s.LBJig.Namespace,
183+
Name: s.LBJig.Name,
184+
Labels: s.LBJig.Labels,
185+
Annotations: make(map[string]string, len(s.DefaultAnnotations)+len(extraAnnotations)),
186+
},
187+
Spec: v1.ServiceSpec{
188+
Type: v1.ServiceTypeLoadBalancer,
189+
SessionAffinity: v1.ServiceAffinityNone,
190+
Selector: s.LBJig.Labels,
191+
Ports: []v1.ServicePort{
61192
{
62193
Name: "http",
63194
Protocol: v1.ProtocolTCP,
64195
Port: int32(80),
65-
TargetPort: intstr.FromInt(80),
196+
TargetPort: intstr.FromInt(int(s.PodPort)),
66197
},
67198
{
68199
Name: "https",
69200
Protocol: v1.ProtocolTCP,
70201
Port: int32(443),
71-
TargetPort: intstr.FromInt(80),
202+
TargetPort: intstr.FromInt(int(s.PodPort)),
72203
},
73-
}
204+
},
205+
},
206+
}
207+
208+
// add default annotations - can be overriden by extra annotations
209+
for aK, aV := range s.DefaultAnnotations {
210+
svc.Annotations[aK] = aV
211+
}
212+
213+
// append test case annotations to the service
214+
for aK, aV := range extraAnnotations {
215+
svc.Annotations[aK] = aV
216+
}
217+
218+
return svc
219+
}
220+
221+
// buildReplicationController creates a replication controller wrapper for the test framework.
222+
// buildReplicationController is basaed on newRCTemplate() from the test, which not provide
223+
// customization to bind in non-privileged ports.
224+
// TODO(mtulio): v1.33+[2] moved from RC to Deployments on tests, we must do the same to use Run()
225+
// when the test framework is updated.
226+
// [1] https://github.com/kubernetes/kubernetes/blob/89d95c9713a8fd189e8ad555120838b3c4f888d1/test/e2e/framework/service/jig.go#L636
227+
// [2] https://github.com/kubernetes/kubernetes/issues/119021
228+
func (s *configServiceLB) buildReplicationController() func(rc *v1.ReplicationController) {
229+
return func(rc *v1.ReplicationController) {
230+
var replicas int32 = 1
231+
var grace int64 = 3 // so we don't race with kube-proxy when scaling up/down
232+
rc.ObjectMeta = metav1.ObjectMeta{
233+
Namespace: s.LBJig.Namespace,
234+
Name: s.LBJig.Name,
235+
Labels: s.LBJig.Labels,
236+
}
237+
rc.Spec = v1.ReplicationControllerSpec{
238+
Replicas: &replicas,
239+
Selector: s.LBJig.Labels,
240+
Template: &v1.PodTemplateSpec{
241+
ObjectMeta: metav1.ObjectMeta{
242+
Labels: s.LBJig.Labels,
243+
},
244+
Spec: v1.PodSpec{
245+
Containers: []v1.Container{
246+
{
247+
Name: "netexec",
248+
Image: imageutils.GetE2EImage(imageutils.Agnhost),
249+
Args: []string{
250+
"netexec",
251+
fmt.Sprintf("--http-port=%d", s.PodPort),
252+
fmt.Sprintf("--udp-port=%d", s.PodPort),
253+
},
254+
ReadinessProbe: &v1.Probe{
255+
PeriodSeconds: 3,
256+
ProbeHandler: v1.ProbeHandler{
257+
HTTPGet: &v1.HTTPGetAction{
258+
Port: intstr.FromInt(int(s.PodPort)),
259+
Path: "/hostName",
260+
},
261+
},
262+
},
263+
},
264+
},
265+
TerminationGracePeriodSeconds: &grace,
266+
},
267+
},
74268
}
269+
}
270+
}
75271

76-
lbService, err := lbJig.CreateLoadBalancerService(loadBalancerCreateTimeout, serviceUpdateFunc)
77-
framework.ExpectNoError(err)
272+
// getLBTargetCount verifies the number of registered targets for a given LBv2 DNS name matches the expected count.
273+
// The steps includes:
274+
// 1. Get Load Balancer ARN from DNS name extracted from service Status.LoadBalancer.Ingress[0].Hostname
275+
// 2. List listeners for the load balancer
276+
// 3. Get target groups attached to listeners
277+
// 4. Count registered targets in target groups
278+
// 5. Verify count matches number of worker nodes
279+
func getLBTargetCount(ctx context.Context, lbDNSName string, expectedTargets int) error {
280+
// Load AWS config
281+
cfg, err := config.LoadDefaultConfig(ctx)
282+
if err != nil {
283+
return fmt.Errorf("unable to load AWS config: %v", err)
284+
}
285+
elbClient := elbv2.NewFromConfig(cfg)
78286

79-
By("creating a pod to be part of the TCP service " + serviceName)
80-
_, err = lbJig.Run(nil)
81-
framework.ExpectNoError(err)
287+
// Get Load Balancer ARN from DNS name
288+
describeLBs, err := elbClient.DescribeLoadBalancers(ctx, &elbv2.DescribeLoadBalancersInput{})
289+
if err != nil {
290+
return fmt.Errorf("failed to describe load balancers: %v", err)
291+
}
292+
var lbARN string
293+
for _, lb := range describeLBs.LoadBalancers {
294+
if strings.EqualFold(aws.ToString(lb.DNSName), lbDNSName) {
295+
lbARN = aws.ToString(lb.LoadBalancerArn)
296+
break
297+
}
298+
}
299+
if lbARN == "" {
300+
return fmt.Errorf("could not find LB with DNS name: %s", lbDNSName)
301+
}
82302

83-
By("hitting the TCP service's LB External IP")
84-
svcPort := int(lbService.Spec.Ports[0].Port)
85-
ingressIP := e2eservice.GetIngressPoint(&lbService.Status.LoadBalancer.Ingress[0])
86-
framework.Logf("Load balancer's ingress IP: %s", ingressIP)
303+
// List listeners for the load balancer
304+
listenersOut, err := elbClient.DescribeListeners(ctx, &elbv2.DescribeListenersInput{
305+
LoadBalancerArn: aws.String(lbARN),
306+
})
307+
if err != nil {
308+
return fmt.Errorf("failed to describe listeners: %v", err)
309+
}
87310

88-
e2eservice.TestReachableHTTP(ingressIP, svcPort, e2eservice.LoadBalancerLagTimeoutAWS)
311+
// Get target groups attached to listeners
312+
targetGroupARNs := map[string]struct{}{}
313+
for _, listener := range listenersOut.Listeners {
314+
if len(targetGroupARNs) > 0 {
315+
break
316+
}
317+
for _, action := range listener.DefaultActions {
318+
if action.TargetGroupArn != nil {
319+
targetGroupARNs[aws.ToString(action.TargetGroupArn)] = struct{}{}
320+
break
321+
}
322+
}
323+
}
89324

90-
// Update the service to cluster IP
91-
By("changing TCP service back to type=ClusterIP")
92-
_, err = lbJig.UpdateService(func(s *v1.Service) {
93-
s.Spec.Type = v1.ServiceTypeClusterIP
325+
// Count registered targets in target groups
326+
totalTargets := 0
327+
for tgARN := range targetGroupARNs {
328+
tgHealth, err := elbClient.DescribeTargetHealth(ctx, &elbv2.DescribeTargetHealthInput{
329+
TargetGroupArn: aws.String(tgARN),
94330
})
95-
framework.ExpectNoError(err)
331+
if err != nil {
332+
return fmt.Errorf("failed to describe target health for TG %s: %v", tgARN, err)
333+
}
334+
totalTargets += len(tgHealth.TargetHealthDescriptions)
335+
}
96336

97-
// Wait for the load balancer to be destroyed asynchronously
98-
_, err = lbJig.WaitForLoadBalancerDestroy(ingressIP, svcPort, loadBalancerCreateTimeout)
99-
framework.ExpectNoError(err)
100-
})
101-
})
337+
// Verify count matches number of worker nodes
338+
if totalTargets != expectedTargets {
339+
return fmt.Errorf("target count mismatch: expected %d, got %d", expectedTargets, totalTargets)
340+
}
341+
return nil
342+
}

0 commit comments

Comments
 (0)