@@ -14,15 +14,27 @@ limitations under the License.
14
14
package e2e
15
15
16
16
import (
17
+ "context"
18
+ "fmt"
19
+ "strings"
20
+
21
+ "github.com/onsi/ginkgo/v2"
17
22
. "github.com/onsi/ginkgo/v2"
18
23
v1 "k8s.io/api/core/v1"
24
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
25
"k8s.io/apimachinery/pkg/util/intstr"
20
26
clientset "k8s.io/client-go/kubernetes"
21
27
"k8s.io/kubernetes/test/e2e/framework"
22
28
e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
29
+ imageutils "k8s.io/kubernetes/test/utils/image"
23
30
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"
24
35
)
25
36
37
+ // loadbalancer tests
26
38
var _ = Describe ("[cloud-provider-aws-e2e] loadbalancer" , func () {
27
39
f := framework .NewDefaultFramework ("cloud-provider-aws" )
28
40
f .NamespacePodSecurityEnforceLevel = admissionapi .LevelPrivileged
@@ -41,61 +53,290 @@ var _ = Describe("[cloud-provider-aws-e2e] loadbalancer", func() {
41
53
// After each test
42
54
})
43
55
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
+ })
47
158
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
50
164
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
+ }
53
167
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
+ }
58
178
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 {
61
192
{
62
193
Name : "http" ,
63
194
Protocol : v1 .ProtocolTCP ,
64
195
Port : int32 (80 ),
65
- TargetPort : intstr .FromInt (80 ),
196
+ TargetPort : intstr .FromInt (int ( s . PodPort ) ),
66
197
},
67
198
{
68
199
Name : "https" ,
69
200
Protocol : v1 .ProtocolTCP ,
70
201
Port : int32 (443 ),
71
- TargetPort : intstr .FromInt (80 ),
202
+ TargetPort : intstr .FromInt (int ( s . PodPort ) ),
72
203
},
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
+ },
74
268
}
269
+ }
270
+ }
75
271
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 )
78
286
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
+ }
82
302
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
+ }
87
310
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
+ }
89
324
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 ),
94
330
})
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
+ }
96
336
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