@@ -14,15 +14,27 @@ limitations under the License.
1414package e2e
1515
1616import (
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
2638var _ = Describe ("[cloud-provider-aws-e2e] loadbalancer" , func () {
2739 f := framework .NewDefaultFramework ("cloud-provider-aws" )
2840 f .NamespacePodSecurityEnforceLevel = admissionapi .LevelPrivileged
@@ -41,61 +53,291 @@ 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+ }
4799
48- serviceName := "lbconfig-test"
49- framework .Logf ("namespace for load balancer conig test: %s" , ns .Name )
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+ if _ , err := lbConfig .LBJig .Client .CoreV1 ().Services (lbConfig .LBJig .Namespace ).Create (context .TODO (), lbServiceConfig , metav1.CreateOptions {}); err != nil {
121+ framework .ExpectNoError (fmt .Errorf ("failed to create LoadBalancer Service %q: %v" , lbServiceConfig .Name , err ))
122+ }
50123
51- By ("creating a TCP service " + serviceName + " with type=LoadBalancerType in namespace " + ns .Name )
52- lbJig := e2eservice .NewTestJig (cs , ns .Name , serviceName )
124+ ginkgo .By ("waiting for loadbalancer for service " + lbServiceConfig .Namespace + "/" + lbServiceConfig .Name )
125+ lbService , err := lbConfig .LBJig .WaitForLoadBalancer (loadBalancerCreateTimeout )
126+ framework .ExpectNoError (err )
127+
128+ // Run Workloads
129+ By ("creating a pod to be part of the TCP service " + serviceName )
130+ _ , err = lbConfig .LBJig .Run (lbConfig .buildReplicationController ())
131+ framework .ExpectNoError (err )
132+
133+ if tc .PostRunValidations != nil {
134+ By ("running post run validations" )
135+ tc .PostRunValidations (lbConfig , lbService )
136+ }
137+
138+ // Test the Service Endpoint
139+ By ("hitting the TCP service's LB External IP" )
140+ svcPort := int (lbService .Spec .Ports [0 ].Port )
141+ ingressIP := e2eservice .GetIngressPoint (& lbService .Status .LoadBalancer .Ingress [0 ])
142+ framework .Logf ("Load balancer's ingress IP: %s" , ingressIP )
143+
144+ e2eservice .TestReachableHTTP (ingressIP , svcPort , e2eservice .LoadBalancerLagTimeoutAWS )
145+
146+ // Update the service to cluster IP
147+ By ("changing TCP service back to type=ClusterIP" )
148+ _ , err = lbConfig .LBJig .UpdateService (func (s * v1.Service ) {
149+ s .Spec .Type = v1 .ServiceTypeClusterIP
150+ })
151+ framework .ExpectNoError (err )
152+
153+ // Wait for the load balancer to be destroyed asynchronously
154+ _ , err = lbConfig .LBJig .WaitForLoadBalancerDestroy (ingressIP , svcPort , loadBalancerCreateTimeout )
155+ framework .ExpectNoError (err )
156+ })
157+ }
158+ })
53159
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"
160+ // configServiceLB hold loadbalancer test configurations
161+ type configServiceLB struct {
162+ PodPort uint16
163+ PodProtocol v1.Protocol
164+ DefaultAnnotations map [string ]string
58165
59- svc .Annotations = annotations
60- svc .Spec .Ports = []v1.ServicePort {
166+ LBJig * e2eservice.TestJig
167+ }
168+
169+ func newConfigServiceLB () * configServiceLB {
170+ return & configServiceLB {
171+ PodPort : 8080 ,
172+ PodProtocol : v1 .ProtocolTCP ,
173+ DefaultAnnotations : map [string ]string {
174+ "aws-load-balancer-backend-protocol" : "http" ,
175+ "aws-load-balancer-ssl-ports" : "https" ,
176+ },
177+ }
178+ }
179+
180+ func (s * configServiceLB ) buildService (extraAnnotations map [string ]string ) * v1.Service {
181+ svc := & v1.Service {
182+ ObjectMeta : metav1.ObjectMeta {
183+ Namespace : s .LBJig .Namespace ,
184+ Name : s .LBJig .Name ,
185+ Labels : s .LBJig .Labels ,
186+ Annotations : make (map [string ]string , len (s .DefaultAnnotations )+ len (extraAnnotations )),
187+ },
188+ Spec : v1.ServiceSpec {
189+ Type : v1 .ServiceTypeLoadBalancer ,
190+ SessionAffinity : v1 .ServiceAffinityNone ,
191+ Selector : s .LBJig .Labels ,
192+ Ports : []v1.ServicePort {
61193 {
62194 Name : "http" ,
63195 Protocol : v1 .ProtocolTCP ,
64196 Port : int32 (80 ),
65- TargetPort : intstr .FromInt (80 ),
197+ TargetPort : intstr .FromInt (int ( s . PodPort ) ),
66198 },
67199 {
68200 Name : "https" ,
69201 Protocol : v1 .ProtocolTCP ,
70202 Port : int32 (443 ),
71- TargetPort : intstr .FromInt (80 ),
203+ TargetPort : intstr .FromInt (int ( s . PodPort ) ),
72204 },
73- }
205+ },
206+ },
207+ }
208+
209+ // add default annotations - can be overriden by extra annotations
210+ for aK , aV := range s .DefaultAnnotations {
211+ svc .Annotations [aK ] = aV
212+ }
213+
214+ // append test case annotations to the service
215+ for aK , aV := range extraAnnotations {
216+ svc .Annotations [aK ] = aV
217+ }
218+
219+ return svc
220+ }
221+
222+ // buildReplicationController creates a replication controller wrapper for the test framework.
223+ // buildReplicationController is basaed on newRCTemplate() from the test, which not provide
224+ // customization to bind in non-privileged ports.
225+ // TODO(mtulio): v1.33+[2] moved from RC to Deployments on tests, we must do the same to use Run()
226+ // when the test framework is updated.
227+ // [1] https://github.com/kubernetes/kubernetes/blob/89d95c9713a8fd189e8ad555120838b3c4f888d1/test/e2e/framework/service/jig.go#L636
228+ // [2] https://github.com/kubernetes/kubernetes/issues/119021
229+ func (s * configServiceLB ) buildReplicationController () func (rc * v1.ReplicationController ) {
230+ return func (rc * v1.ReplicationController ) {
231+ var replicas int32 = 1
232+ var grace int64 = 3 // so we don't race with kube-proxy when scaling up/down
233+ rc .ObjectMeta = metav1.ObjectMeta {
234+ Namespace : s .LBJig .Namespace ,
235+ Name : s .LBJig .Name ,
236+ Labels : s .LBJig .Labels ,
237+ }
238+ rc .Spec = v1.ReplicationControllerSpec {
239+ Replicas : & replicas ,
240+ Selector : s .LBJig .Labels ,
241+ Template : & v1.PodTemplateSpec {
242+ ObjectMeta : metav1.ObjectMeta {
243+ Labels : s .LBJig .Labels ,
244+ },
245+ Spec : v1.PodSpec {
246+ Containers : []v1.Container {
247+ {
248+ Name : "netexec" ,
249+ Image : imageutils .GetE2EImage (imageutils .Agnhost ),
250+ Args : []string {
251+ "netexec" ,
252+ fmt .Sprintf ("--http-port=%d" , s .PodPort ),
253+ fmt .Sprintf ("--udp-port=%d" , s .PodPort ),
254+ },
255+ ReadinessProbe : & v1.Probe {
256+ PeriodSeconds : 3 ,
257+ ProbeHandler : v1.ProbeHandler {
258+ HTTPGet : & v1.HTTPGetAction {
259+ Port : intstr .FromInt (int (s .PodPort )),
260+ Path : "/hostName" ,
261+ },
262+ },
263+ },
264+ },
265+ },
266+ TerminationGracePeriodSeconds : & grace ,
267+ },
268+ },
74269 }
270+ }
271+ }
75272
76- lbService , err := lbJig .CreateLoadBalancerService (loadBalancerCreateTimeout , serviceUpdateFunc )
77- framework .ExpectNoError (err )
273+ // getLBTargetCount verifies the number of registered targets for a given LBv2 DNS name matches the expected count.
274+ // The steps includes:
275+ // 1. Get Load Balancer ARN from DNS name extracted from service Status.LoadBalancer.Ingress[0].Hostname
276+ // 2. List listeners for the load balancer
277+ // 3. Get target groups attached to listeners
278+ // 4. Count registered targets in target groups
279+ // 5. Verify count matches number of worker nodes
280+ func getLBTargetCount (ctx context.Context , lbDNSName string , expectedTargets int ) error {
281+ // Load AWS config
282+ cfg , err := config .LoadDefaultConfig (ctx )
283+ if err != nil {
284+ return fmt .Errorf ("unable to load AWS config: %v" , err )
285+ }
286+ elbClient := elbv2 .NewFromConfig (cfg )
78287
79- By ("creating a pod to be part of the TCP service " + serviceName )
80- _ , err = lbJig .Run (nil )
81- framework .ExpectNoError (err )
288+ // Get Load Balancer ARN from DNS name
289+ describeLBs , err := elbClient .DescribeLoadBalancers (ctx , & elbv2.DescribeLoadBalancersInput {})
290+ if err != nil {
291+ return fmt .Errorf ("failed to describe load balancers: %v" , err )
292+ }
293+ var lbARN string
294+ for _ , lb := range describeLBs .LoadBalancers {
295+ if strings .EqualFold (aws .ToString (lb .DNSName ), lbDNSName ) {
296+ lbARN = aws .ToString (lb .LoadBalancerArn )
297+ break
298+ }
299+ }
300+ if lbARN == "" {
301+ return fmt .Errorf ("could not find LB with DNS name: %s" , lbDNSName )
302+ }
82303
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 )
304+ // List listeners for the load balancer
305+ listenersOut , err := elbClient .DescribeListeners (ctx , & elbv2.DescribeListenersInput {
306+ LoadBalancerArn : aws .String (lbARN ),
307+ })
308+ if err != nil {
309+ return fmt .Errorf ("failed to describe listeners: %v" , err )
310+ }
87311
88- e2eservice .TestReachableHTTP (ingressIP , svcPort , e2eservice .LoadBalancerLagTimeoutAWS )
312+ // Get target groups attached to listeners
313+ targetGroupARNs := map [string ]struct {}{}
314+ for _ , listener := range listenersOut .Listeners {
315+ if len (targetGroupARNs ) > 0 {
316+ break
317+ }
318+ for _ , action := range listener .DefaultActions {
319+ if action .TargetGroupArn != nil {
320+ targetGroupARNs [aws .ToString (action .TargetGroupArn )] = struct {}{}
321+ break
322+ }
323+ }
324+ }
89325
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
326+ // Count registered targets in target groups
327+ totalTargets := 0
328+ for tgARN := range targetGroupARNs {
329+ tgHealth , err := elbClient .DescribeTargetHealth (ctx , & elbv2.DescribeTargetHealthInput {
330+ TargetGroupArn : aws .String (tgARN ),
94331 })
95- framework .ExpectNoError (err )
332+ if err != nil {
333+ return fmt .Errorf ("failed to describe target health for TG %s: %v" , tgARN , err )
334+ }
335+ totalTargets += len (tgHealth .TargetHealthDescriptions )
336+ }
96337
97- // Wait for the load balancer to be destroyed asynchronously
98- _ , err = lbJig .WaitForLoadBalancerDestroy (ingressIP , svcPort , loadBalancerCreateTimeout )
99- framework .ExpectNoError (err )
100- })
101- })
338+ // Verify count matches number of worker nodes
339+ if totalTargets != expectedTargets {
340+ return fmt .Errorf ("target count mismatch: expected %d, got %d" , expectedTargets , totalTargets )
341+ }
342+ return nil
343+ }
0 commit comments