github.com/cilium/cilium@v1.16.2/operator/pkg/nodeipam/nodesvclb_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package nodeipam 5 6 import ( 7 "bytes" 8 "context" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/require" 13 corev1 "k8s.io/api/core/v1" 14 discoveryv1 "k8s.io/api/discovery/v1" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/types" 17 ctrl "sigs.k8s.io/controller-runtime" 18 "sigs.k8s.io/controller-runtime/pkg/client" 19 "sigs.k8s.io/controller-runtime/pkg/client/fake" 20 21 "github.com/cilium/cilium/pkg/logging" 22 ) 23 24 var ( 25 nodeSvcLbFixtures = []client.Object{ 26 &corev1.Node{ 27 ObjectMeta: metav1.ObjectMeta{ 28 Name: "node-1", 29 }, 30 Status: corev1.NodeStatus{ 31 Addresses: []corev1.NodeAddress{ 32 {Type: corev1.NodeInternalIP, Address: "10.0.0.1"}, 33 {Type: corev1.NodeExternalIP, Address: "2001:0000::1"}, 34 }, 35 }, 36 }, 37 &corev1.Node{ 38 ObjectMeta: metav1.ObjectMeta{ 39 Name: "node-2", 40 }, 41 Status: corev1.NodeStatus{ 42 Addresses: []corev1.NodeAddress{ 43 {Type: corev1.NodeInternalIP, Address: "fc00::2"}, 44 {Type: corev1.NodeExternalIP, Address: "42.0.0.2"}, 45 }, 46 }, 47 }, 48 &corev1.Node{ 49 ObjectMeta: metav1.ObjectMeta{ 50 Name: "node-3", 51 }, 52 Status: corev1.NodeStatus{ 53 Addresses: []corev1.NodeAddress{ 54 {Type: corev1.NodeExternalIP, Address: "2001:0000::3"}, 55 {Type: corev1.NodeExternalIP, Address: "42.0.0.3"}, 56 }, 57 }, 58 }, 59 60 &corev1.Node{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Name: "node-4-excluded", 63 DeletionTimestamp: &metav1.Time{Time: time.Now()}, 64 Finalizers: []string{"myfinalizer"}, 65 }, 66 Status: corev1.NodeStatus{ 67 Addresses: []corev1.NodeAddress{ 68 {Type: corev1.NodeExternalIP, Address: "2001:0000:4"}, 69 {Type: corev1.NodeExternalIP, Address: "42.0.0.4"}, 70 }, 71 }, 72 }, 73 &corev1.Node{ 74 ObjectMeta: metav1.ObjectMeta{ 75 Name: "node-5-excluded", 76 Labels: map[string]string{ 77 corev1.LabelNodeExcludeBalancers: "", 78 }, 79 }, 80 Status: corev1.NodeStatus{ 81 Addresses: []corev1.NodeAddress{ 82 {Type: corev1.NodeExternalIP, Address: "2001:0000:5"}, 83 {Type: corev1.NodeExternalIP, Address: "42.0.0.5"}, 84 }, 85 }, 86 }, 87 &corev1.Node{ 88 ObjectMeta: metav1.ObjectMeta{ 89 Name: "node-6-excluded", 90 }, 91 Spec: corev1.NodeSpec{ 92 Taints: []corev1.Taint{ 93 {Key: toBeDeletedTaint}, 94 }, 95 }, 96 Status: corev1.NodeStatus{ 97 Addresses: []corev1.NodeAddress{ 98 {Type: corev1.NodeExternalIP, Address: "2001:0000:6"}, 99 {Type: corev1.NodeExternalIP, Address: "42.0.0.6"}, 100 }, 101 }, 102 }, 103 104 &discoveryv1.EndpointSlice{ 105 ObjectMeta: metav1.ObjectMeta{ 106 Name: "ipv4-internal", 107 Namespace: "default", 108 Labels: map[string]string{discoveryv1.LabelServiceName: "ipv4-internal"}, 109 }, 110 Endpoints: []discoveryv1.Endpoint{ 111 {NodeName: stringPtr("node-1")}, 112 {NodeName: stringPtr("node-2"), Conditions: discoveryv1.EndpointConditions{Ready: boolPtr(false)}}, 113 }, 114 }, 115 &corev1.Service{ 116 ObjectMeta: metav1.ObjectMeta{ 117 Name: "ipv4-internal", 118 Namespace: "default", 119 }, 120 Spec: corev1.ServiceSpec{ 121 Type: corev1.ServiceTypeLoadBalancer, 122 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, 123 LoadBalancerClass: &nodeSvcLBClass, 124 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 125 }, 126 }, 127 128 &discoveryv1.EndpointSlice{ 129 ObjectMeta: metav1.ObjectMeta{ 130 Name: "ipv4-external", 131 Namespace: "default", 132 Labels: map[string]string{discoveryv1.LabelServiceName: "ipv4-external"}, 133 }, 134 Endpoints: []discoveryv1.Endpoint{ 135 {NodeName: stringPtr("node-1")}, 136 {NodeName: stringPtr("node-2")}, 137 }, 138 }, 139 &corev1.Service{ 140 ObjectMeta: metav1.ObjectMeta{ 141 Name: "ipv4-external", 142 Namespace: "default", 143 }, 144 Spec: corev1.ServiceSpec{ 145 Type: corev1.ServiceTypeLoadBalancer, 146 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, 147 LoadBalancerClass: &nodeSvcLBClass, 148 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 149 }, 150 }, 151 152 &discoveryv1.EndpointSlice{ 153 ObjectMeta: metav1.ObjectMeta{ 154 Name: "ipv6-internal", 155 Namespace: "default", 156 Labels: map[string]string{discoveryv1.LabelServiceName: "ipv6-internal"}, 157 }, 158 Endpoints: []discoveryv1.Endpoint{ 159 {NodeName: stringPtr("node-2")}, 160 }, 161 }, 162 &corev1.Service{ 163 ObjectMeta: metav1.ObjectMeta{ 164 Name: "ipv6-internal", 165 Namespace: "default", 166 }, 167 Spec: corev1.ServiceSpec{ 168 Type: corev1.ServiceTypeLoadBalancer, 169 IPFamilies: []corev1.IPFamily{corev1.IPv6Protocol}, 170 LoadBalancerClass: &nodeSvcLBClass, 171 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 172 }, 173 }, 174 175 &discoveryv1.EndpointSlice{ 176 ObjectMeta: metav1.ObjectMeta{ 177 Name: "ipv6-external", 178 Namespace: "default", 179 Labels: map[string]string{discoveryv1.LabelServiceName: "ipv6-external"}, 180 }, 181 Endpoints: []discoveryv1.Endpoint{ 182 {NodeName: stringPtr("node-1")}, 183 {NodeName: stringPtr("node-2")}, 184 }, 185 }, 186 &corev1.Service{ 187 ObjectMeta: metav1.ObjectMeta{ 188 Name: "ipv6-external", 189 Namespace: "default", 190 }, 191 Spec: corev1.ServiceSpec{ 192 Type: corev1.ServiceTypeLoadBalancer, 193 IPFamilies: []corev1.IPFamily{corev1.IPv6Protocol}, 194 LoadBalancerClass: &nodeSvcLBClass, 195 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 196 }, 197 }, 198 199 &discoveryv1.EndpointSlice{ 200 ObjectMeta: metav1.ObjectMeta{ 201 Name: "dualstack-external", 202 Namespace: "default", 203 Labels: map[string]string{discoveryv1.LabelServiceName: "dualstack-external"}, 204 }, 205 Endpoints: []discoveryv1.Endpoint{ 206 {NodeName: stringPtr("node-1")}, 207 {NodeName: stringPtr("node-2")}, 208 {NodeName: stringPtr("does-not-exist")}, 209 }, 210 }, 211 &corev1.Service{ 212 ObjectMeta: metav1.ObjectMeta{ 213 Name: "dualstack-external", 214 Namespace: "default", 215 }, 216 Spec: corev1.ServiceSpec{ 217 Type: corev1.ServiceTypeLoadBalancer, 218 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}, 219 LoadBalancerClass: &nodeSvcLBClass, 220 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 221 }, 222 }, 223 224 &corev1.Service{ 225 ObjectMeta: metav1.ObjectMeta{ 226 Name: "etp-cluster", 227 Namespace: "default", 228 }, 229 Spec: corev1.ServiceSpec{ 230 Type: corev1.ServiceTypeLoadBalancer, 231 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, 232 LoadBalancerClass: &nodeSvcLBClass, 233 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyCluster, 234 }, 235 }, 236 237 &discoveryv1.EndpointSlice{ 238 ObjectMeta: metav1.ObjectMeta{ 239 Name: "not-supported-1", 240 Namespace: "default", 241 Labels: map[string]string{discoveryv1.LabelServiceName: "not-supported-1"}, 242 }, 243 Endpoints: []discoveryv1.Endpoint{ 244 {NodeName: stringPtr("node-1")}, 245 {NodeName: stringPtr("node-2")}, 246 }, 247 }, 248 &corev1.Service{ 249 ObjectMeta: metav1.ObjectMeta{ 250 Name: "not-supported-1", 251 Namespace: "default", 252 }, 253 Spec: corev1.ServiceSpec{ 254 Type: corev1.ServiceTypeLoadBalancer, 255 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}, 256 }, 257 Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{ 258 Ingress: []corev1.LoadBalancerIngress{{IP: "100.100.100.100"}}, 259 }}, 260 }, 261 262 &discoveryv1.EndpointSlice{ 263 ObjectMeta: metav1.ObjectMeta{ 264 Name: "not-supported-2", 265 Namespace: "default", 266 Labels: map[string]string{discoveryv1.LabelServiceName: "not-supported-2"}, 267 }, 268 Endpoints: []discoveryv1.Endpoint{ 269 {NodeName: stringPtr("node-1")}, 270 {NodeName: stringPtr("node-2")}, 271 }, 272 }, 273 &corev1.Service{ 274 ObjectMeta: metav1.ObjectMeta{ 275 Name: "not-supported-2", 276 Namespace: "default", 277 }, 278 Spec: corev1.ServiceSpec{ 279 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}, 280 LoadBalancerClass: &nodeSvcLBClass, 281 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 282 }, 283 Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{ 284 Ingress: []corev1.LoadBalancerIngress{{IP: "100.100.100.100"}}, 285 }}, 286 }, 287 } 288 289 nodeSvcLabelFixtures = []client.Object{ 290 &corev1.Node{ 291 ObjectMeta: metav1.ObjectMeta{ 292 Name: "node-1", 293 Labels: map[string]string{"ingress-ready": "true", "all": "true", "group": "first"}, 294 }, 295 Status: corev1.NodeStatus{ 296 Addresses: []corev1.NodeAddress{ 297 {Type: corev1.NodeInternalIP, Address: "10.0.0.1"}, 298 }, 299 }, 300 }, 301 &corev1.Node{ 302 ObjectMeta: metav1.ObjectMeta{ 303 Name: "node-2", 304 Labels: map[string]string{"all": "true", "group": "notfirst", "test/label": "is-good"}, 305 }, 306 Status: corev1.NodeStatus{ 307 Addresses: []corev1.NodeAddress{ 308 {Type: corev1.NodeInternalIP, Address: "10.0.0.2"}, 309 }, 310 }, 311 }, 312 &corev1.Node{ 313 ObjectMeta: metav1.ObjectMeta{ 314 Name: "node-3", 315 Labels: map[string]string{"all": "true", "group": "notfirst"}, 316 }, 317 Status: corev1.NodeStatus{ 318 Addresses: []corev1.NodeAddress{ 319 {Type: corev1.NodeInternalIP, Address: "10.0.0.3"}, 320 }, 321 }, 322 }, 323 &corev1.Service{ 324 ObjectMeta: metav1.ObjectMeta{ 325 Name: "svclabels", 326 Namespace: "default", 327 }, 328 Spec: corev1.ServiceSpec{ 329 Type: corev1.ServiceTypeLoadBalancer, 330 IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, 331 LoadBalancerClass: &nodeSvcLBClass, 332 }, 333 }, 334 } 335 ) 336 337 func stringPtr(str string) *string { 338 return &str 339 } 340 func boolPtr(boolean bool) *bool { 341 return &boolean 342 } 343 344 func Test_httpRouteReconciler_Reconcile(t *testing.T) { 345 c := fake.NewClientBuilder(). 346 WithObjects(nodeSvcLbFixtures...). 347 WithStatusSubresource(&corev1.Service{}). 348 Build() 349 r := &nodeSvcLBReconciler{Client: c, Logger: logging.DefaultLogger} 350 351 t.Run("unsupported service reset", func(t *testing.T) { 352 for _, name := range []string{"not-supported-1", "not-supported-2"} { 353 key := types.NamespacedName{ 354 Name: name, 355 Namespace: "default", 356 } 357 result, err := r.Reconcile(context.Background(), ctrl.Request{ 358 NamespacedName: key, 359 }) 360 361 require.NoError(t, err) 362 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 363 364 svc := &corev1.Service{} 365 err = c.Get(context.Background(), key, svc) 366 367 require.NoError(t, err) 368 // It did not change the IPs already advertised 369 require.Len(t, svc.Status.LoadBalancer.Ingress, 1) 370 require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "100.100.100.100") 371 } 372 }) 373 374 t.Run("single address test in single stack", func(t *testing.T) { 375 for _, param := range []struct { 376 name string 377 address string 378 }{ 379 {name: "ipv4-internal", address: "10.0.0.1"}, 380 {name: "ipv4-external", address: "42.0.0.2"}, 381 {name: "ipv6-internal", address: "fc00::2"}, 382 {name: "ipv6-external", address: "2001:0000::1"}, 383 } { 384 key := types.NamespacedName{ 385 Name: param.name, 386 Namespace: "default", 387 } 388 result, err := r.Reconcile(context.Background(), ctrl.Request{ 389 NamespacedName: key, 390 }) 391 392 require.NoError(t, err) 393 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 394 395 svc := &corev1.Service{} 396 err = c.Get(context.Background(), key, svc) 397 398 require.NoError(t, err) 399 require.Len(t, svc.Status.LoadBalancer.Ingress, 1) 400 require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, param.address) 401 } 402 }) 403 404 t.Run("dual stack", func(t *testing.T) { 405 key := types.NamespacedName{ 406 Name: "dualstack-external", 407 Namespace: "default", 408 } 409 result, err := r.Reconcile(context.Background(), ctrl.Request{ 410 NamespacedName: key, 411 }) 412 413 require.NoError(t, err) 414 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 415 416 svc := &corev1.Service{} 417 err = c.Get(context.Background(), key, svc) 418 419 require.NoError(t, err) 420 require.Len(t, svc.Status.LoadBalancer.Ingress, 2) 421 require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "2001:0000::1") 422 require.Equal(t, svc.Status.LoadBalancer.Ingress[1].IP, "42.0.0.2") 423 }) 424 425 // 426 t.Run("external traffic policy cluster", func(t *testing.T) { 427 key := types.NamespacedName{ 428 Name: "etp-cluster", 429 Namespace: "default", 430 } 431 result, err := r.Reconcile(context.Background(), ctrl.Request{ 432 NamespacedName: key, 433 }) 434 435 require.NoError(t, err) 436 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 437 438 svc := &corev1.Service{} 439 err = c.Get(context.Background(), key, svc) 440 441 require.NoError(t, err) 442 require.Len(t, svc.Status.LoadBalancer.Ingress, 2) 443 require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "42.0.0.2") 444 require.Equal(t, svc.Status.LoadBalancer.Ingress[1].IP, "42.0.0.3") 445 }) 446 } 447 448 func Test_CiliumResources_Reconcile(t *testing.T) { 449 c := fake.NewClientBuilder(). 450 WithObjects(nodeSvcLabelFixtures...). 451 WithStatusSubresource(&corev1.Service{}). 452 Build() 453 r := &nodeSvcLBReconciler{Client: c, Logger: logging.DefaultLogger} 454 455 key := types.NamespacedName{ 456 Name: "svclabels", 457 Namespace: "default", 458 } 459 460 t.Run("Managed Resource", func(t *testing.T) { 461 ctx := context.Background() 462 result, err := r.Reconcile(ctx, ctrl.Request{ 463 NamespacedName: key, 464 }) 465 466 require.NoError(t, err) 467 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 468 469 svc := &corev1.Service{} 470 err = c.Get(ctx, key, svc) 471 472 require.NoError(t, err) 473 require.Len(t, svc.Status.LoadBalancer.Ingress, 3) 474 var ips []string 475 for _, v := range svc.Status.LoadBalancer.Ingress { 476 ips = append(ips, v.IP) 477 } 478 require.Equal(t, ips, []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}) 479 }) 480 481 t.Run("Node Label Filter", func(t *testing.T) { 482 ctx := context.Background() 483 484 for _, param := range []struct { 485 labelFilter string 486 results []string 487 }{ 488 {labelFilter: "all=true", results: []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}}, 489 {labelFilter: "ingress-ready=true", results: []string{"10.0.0.1"}}, 490 {labelFilter: "group=notfirst", results: []string{"10.0.0.2", "10.0.0.3"}}, 491 {labelFilter: "group notin (first),test/label=is-good", results: []string{"10.0.0.2"}}, 492 } { 493 svc := &corev1.Service{} 494 _ = c.Get(ctx, key, svc) 495 // Add the label to the service which should return on the first node 496 svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: param.labelFilter} 497 _ = c.Update(ctx, svc) 498 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: key}) 499 500 require.NoError(t, err) 501 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 502 503 svc = &corev1.Service{} 504 err = c.Get(ctx, key, svc) 505 506 require.NoError(t, err) 507 require.NotNil(t, svc.Annotations[nodeSvcLBMatchLabelsAnnotation]) 508 require.Len(t, svc.Status.LoadBalancer.Ingress, len(param.results)) 509 510 var ips []string 511 for _, v := range svc.Status.LoadBalancer.Ingress { 512 ips = append(ips, v.IP) 513 } 514 require.Equal(t, ips, param.results) 515 } 516 }) 517 518 t.Run("Bad Node Label Filter", func(t *testing.T) { 519 ctx := context.Background() 520 svc := &corev1.Service{} 521 _ = c.Get(ctx, key, svc) 522 // Add the label to the service which should return on the first node 523 svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: "this is completely/bad=!;lf"} 524 _ = c.Update(ctx, svc) 525 result, err := r.Reconcile(ctx, ctrl.Request{ 526 NamespacedName: key, 527 }) 528 529 require.Error(t, err) 530 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 531 532 }) 533 534 t.Run("Ensure Warning raised if no Nodes found using configured label selector", func(t *testing.T) { 535 var buf bytes.Buffer 536 logger := logging.DefaultLogger 537 logger.SetOutput(&buf) 538 539 r.Logger = logger 540 541 ctx := context.Background() 542 svc := &corev1.Service{} 543 _ = c.Get(ctx, key, svc) 544 // Add the label to the service which should return on the first node 545 svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: "foo=bar"} 546 _ = c.Update(ctx, svc) 547 result, err := r.Reconcile(ctx, ctrl.Request{ 548 NamespacedName: key, 549 }) 550 551 require.NoError(t, err) 552 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 553 require.Contains(t, buf.String(), "level=warning msg=\"No Nodes found with configured label selector\"") 554 }) 555 }