@@ -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,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