k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controlplane/reconcilers/endpointsadapter_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package reconcilers 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 24 corev1 "k8s.io/api/core/v1" 25 discovery "k8s.io/api/discovery/v1" 26 apiequality "k8s.io/apimachinery/pkg/api/equality" 27 "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 "k8s.io/client-go/kubernetes/fake" 32 ) 33 34 var noEndpoints = &corev1.Endpoints{} 35 36 func TestEndpointsAdapterGet(t *testing.T) { 37 endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4"}) 38 39 testCases := map[string]struct { 40 expectedError error 41 expectedEndpoints *corev1.Endpoints 42 initialState []runtime.Object 43 namespaceParam string 44 nameParam string 45 }{ 46 "single-existing-endpoints": { 47 expectedError: nil, 48 expectedEndpoints: endpoints1, 49 initialState: []runtime.Object{endpoints1, epSlice1}, 50 namespaceParam: "testing", 51 nameParam: "foo", 52 }, 53 "endpoints exists, endpointslice does not": { 54 expectedError: nil, 55 expectedEndpoints: endpoints1, 56 initialState: []runtime.Object{endpoints1}, 57 namespaceParam: "testing", 58 nameParam: "foo", 59 }, 60 "endpointslice exists, endpoints does not": { 61 expectedError: errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"), 62 expectedEndpoints: noEndpoints, 63 initialState: []runtime.Object{epSlice1}, 64 namespaceParam: "testing", 65 nameParam: "foo", 66 }, 67 "wrong-namespace": { 68 expectedError: errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"), 69 expectedEndpoints: noEndpoints, 70 initialState: []runtime.Object{endpoints1, epSlice1}, 71 namespaceParam: "foo", 72 nameParam: "foo", 73 }, 74 "wrong-name": { 75 expectedError: errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"), 76 expectedEndpoints: noEndpoints, 77 initialState: []runtime.Object{endpoints1, epSlice1}, 78 namespaceParam: "testing", 79 nameParam: "bar", 80 }, 81 } 82 83 for name, testCase := range testCases { 84 t.Run(name, func(t *testing.T) { 85 client := fake.NewSimpleClientset(testCase.initialState...) 86 epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1()) 87 88 endpoints, err := epAdapter.Get(testCase.namespaceParam, testCase.nameParam, metav1.GetOptions{}) 89 90 if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) { 91 t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err) 92 } 93 94 if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedEndpoints) { 95 t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedEndpoints, endpoints) 96 } 97 }) 98 } 99 } 100 101 func TestEndpointsAdapterCreate(t *testing.T) { 102 endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.3", "10.1.2.4"}) 103 104 // even if an Endpoints resource includes an IPv6 address, it should not be 105 // included in the corresponding EndpointSlice. 106 endpoints2, _ := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.5", "10.1.2.6", "1234::5678:0000:0000:9abc:def0"}) 107 _, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.5", "10.1.2.6"}) 108 109 // ensure that Endpoints with only IPv6 addresses result in EndpointSlice 110 // with an IPv6 address type. 111 endpoints3, epSlice3 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"1234::5678:0000:0000:9abc:def0"}) 112 epSlice3.AddressType = discovery.AddressTypeIPv6 113 114 testCases := map[string]struct { 115 expectedError error 116 expectedResult *corev1.Endpoints 117 expectCreate []runtime.Object 118 expectUpdate []runtime.Object 119 initialState []runtime.Object 120 namespaceParam string 121 endpointsParam *corev1.Endpoints 122 }{ 123 "single-endpoint": { 124 expectedError: nil, 125 expectedResult: endpoints1, 126 expectCreate: []runtime.Object{endpoints1, epSlice1}, 127 initialState: []runtime.Object{}, 128 namespaceParam: endpoints1.Namespace, 129 endpointsParam: endpoints1, 130 }, 131 "single-endpoint-partial-ipv6": { 132 expectedError: nil, 133 expectedResult: endpoints2, 134 expectCreate: []runtime.Object{endpoints2, epSlice2}, 135 initialState: []runtime.Object{}, 136 namespaceParam: endpoints2.Namespace, 137 endpointsParam: endpoints2, 138 }, 139 "single-endpoint-full-ipv6": { 140 expectedError: nil, 141 expectedResult: endpoints3, 142 expectCreate: []runtime.Object{endpoints3, epSlice3}, 143 initialState: []runtime.Object{}, 144 namespaceParam: endpoints3.Namespace, 145 endpointsParam: endpoints3, 146 }, 147 "existing-endpoints": { 148 expectedError: errors.NewAlreadyExists(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"), 149 expectedResult: noEndpoints, 150 initialState: []runtime.Object{endpoints1, epSlice1}, 151 namespaceParam: endpoints1.Namespace, 152 endpointsParam: endpoints1, 153 154 // We expect the create to be attempted, we just also expect it to fail 155 expectCreate: []runtime.Object{endpoints1}, 156 }, 157 "existing-endpointslice-incorrect": { 158 // No error when we need to create the Endpoints but the correct 159 // EndpointSlice already exists 160 expectedError: nil, 161 expectedResult: endpoints1, 162 expectCreate: []runtime.Object{endpoints1}, 163 initialState: []runtime.Object{epSlice1}, 164 namespaceParam: endpoints1.Namespace, 165 endpointsParam: endpoints1, 166 }, 167 "existing-endpointslice-correct": { 168 // No error when we need to create the Endpoints but an incorrect 169 // EndpointSlice already exists 170 expectedError: nil, 171 expectedResult: endpoints2, 172 expectCreate: []runtime.Object{endpoints2}, 173 expectUpdate: []runtime.Object{epSlice2}, 174 initialState: []runtime.Object{epSlice1}, 175 namespaceParam: endpoints2.Namespace, 176 endpointsParam: endpoints2, 177 }, 178 } 179 180 for name, testCase := range testCases { 181 t.Run(name, func(t *testing.T) { 182 client := fake.NewSimpleClientset(testCase.initialState...) 183 epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1()) 184 185 endpoints, err := epAdapter.Create(testCase.namespaceParam, testCase.endpointsParam) 186 187 if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) { 188 t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err) 189 } 190 191 if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedResult) { 192 t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedResult, endpoints) 193 } 194 195 err = verifyCreatesAndUpdates(client, testCase.expectCreate, testCase.expectUpdate) 196 if err != nil { 197 t.Errorf("unexpected error in side effects: %v", err) 198 } 199 }) 200 } 201 } 202 203 func TestEndpointsAdapterUpdate(t *testing.T) { 204 endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.3", "10.1.2.4"}) 205 endpoints2, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"}) 206 endpoints3, _ := generateEndpointsAndSlice("bar", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"}) 207 208 // ensure that EndpointSlice with deprecated IP address type is replaced 209 // with one that has an IPv4 address type. 210 endpoints4, _ := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"}) 211 _, epSlice4IP := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"}) 212 // "IP" is a deprecated address type, ensuring that it is handled properly. 213 epSlice4IP.AddressType = discovery.AddressType("IP") 214 _, epSlice4IPv4 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"}) 215 216 testCases := map[string]struct { 217 expectedError error 218 expectedResult *corev1.Endpoints 219 expectCreate []runtime.Object 220 expectUpdate []runtime.Object 221 initialState []runtime.Object 222 namespaceParam string 223 endpointsParam *corev1.Endpoints 224 }{ 225 "single-existing-endpoints-no-change": { 226 expectedError: nil, 227 expectedResult: endpoints1, 228 initialState: []runtime.Object{endpoints1, epSlice1}, 229 namespaceParam: "testing", 230 endpointsParam: endpoints1, 231 232 // Even though there's no change, we still expect Update() to be 233 // called, because this unit test ALWAYS calls Update(). 234 expectUpdate: []runtime.Object{endpoints1}, 235 }, 236 "existing-endpointslice-replaced-with-updated-ipv4-address-type": { 237 expectedError: nil, 238 expectedResult: endpoints4, 239 initialState: []runtime.Object{endpoints4, epSlice4IP}, 240 namespaceParam: "testing", 241 endpointsParam: endpoints4, 242 243 // When AddressType changes, we Delete+Create the EndpointSlice, 244 // so that shows up in expectCreate, not expectUpdate. 245 expectUpdate: []runtime.Object{endpoints4}, 246 expectCreate: []runtime.Object{epSlice4IPv4}, 247 }, 248 "add-ports-and-ips": { 249 expectedError: nil, 250 expectedResult: endpoints2, 251 expectUpdate: []runtime.Object{endpoints2, epSlice2}, 252 initialState: []runtime.Object{endpoints1, epSlice1}, 253 namespaceParam: "testing", 254 endpointsParam: endpoints2, 255 }, 256 "endpoints-correct-endpointslice-wrong": { 257 expectedError: nil, 258 expectedResult: endpoints2, 259 expectUpdate: []runtime.Object{endpoints2, epSlice2}, 260 initialState: []runtime.Object{endpoints2, epSlice1}, 261 namespaceParam: "testing", 262 endpointsParam: endpoints2, 263 }, 264 "endpointslice-correct-endpoints-wrong": { 265 expectedError: nil, 266 expectedResult: endpoints2, 267 expectUpdate: []runtime.Object{endpoints2}, 268 initialState: []runtime.Object{endpoints1, epSlice2}, 269 namespaceParam: "testing", 270 endpointsParam: endpoints2, 271 }, 272 "wrong-endpoints": { 273 expectedError: errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"), 274 expectedResult: noEndpoints, 275 expectUpdate: []runtime.Object{endpoints3}, 276 initialState: []runtime.Object{endpoints1, epSlice1}, 277 namespaceParam: "testing", 278 endpointsParam: endpoints3, 279 }, 280 "missing-endpoints": { 281 expectedError: errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"), 282 expectedResult: noEndpoints, 283 initialState: []runtime.Object{endpoints1, epSlice1}, 284 namespaceParam: "testing", 285 endpointsParam: endpoints3, 286 287 // We expect the update to be attempted, we just also expect it to fail 288 expectUpdate: []runtime.Object{endpoints3}, 289 }, 290 "missing-endpointslice": { 291 // No error when we need to update the Endpoints but the 292 // EndpointSlice doesn't exist 293 expectedError: nil, 294 expectedResult: endpoints1, 295 expectUpdate: []runtime.Object{endpoints1}, 296 expectCreate: []runtime.Object{epSlice1}, 297 initialState: []runtime.Object{endpoints2}, 298 namespaceParam: "testing", 299 endpointsParam: endpoints1, 300 }, 301 } 302 303 for name, testCase := range testCases { 304 t.Run(name, func(t *testing.T) { 305 client := fake.NewSimpleClientset(testCase.initialState...) 306 epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1()) 307 308 endpoints, err := epAdapter.Update(testCase.namespaceParam, testCase.endpointsParam) 309 310 if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) { 311 t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err) 312 } 313 314 if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedResult) { 315 t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedResult, endpoints) 316 } 317 318 err = verifyCreatesAndUpdates(client, testCase.expectCreate, testCase.expectUpdate) 319 if err != nil { 320 t.Errorf("unexpected error in side effects: %v", err) 321 } 322 }) 323 } 324 } 325 326 func generateEndpointsAndSlice(name, namespace string, ports []int, addresses []string) (*corev1.Endpoints, *discovery.EndpointSlice) { 327 trueBool := true 328 329 epSlice := &discovery.EndpointSlice{ 330 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, 331 AddressType: discovery.AddressTypeIPv4, 332 } 333 epSlice.Labels = map[string]string{discovery.LabelServiceName: name} 334 subset := corev1.EndpointSubset{} 335 336 for i, port := range ports { 337 endpointPort := corev1.EndpointPort{ 338 Name: fmt.Sprintf("port-%d", i), 339 Port: int32(port), 340 Protocol: corev1.ProtocolTCP, 341 } 342 subset.Ports = append(subset.Ports, endpointPort) 343 epSlice.Ports = append(epSlice.Ports, discovery.EndpointPort{ 344 Name: &endpointPort.Name, 345 Port: &endpointPort.Port, 346 Protocol: &endpointPort.Protocol, 347 }) 348 } 349 350 for i, address := range addresses { 351 endpointAddress := corev1.EndpointAddress{ 352 IP: address, 353 TargetRef: &corev1.ObjectReference{ 354 Kind: "Pod", 355 Name: fmt.Sprintf("pod-%d", i), 356 }, 357 } 358 359 subset.Addresses = append(subset.Addresses, endpointAddress) 360 361 epSlice.Endpoints = append(epSlice.Endpoints, discovery.Endpoint{ 362 Addresses: []string{endpointAddress.IP}, 363 TargetRef: endpointAddress.TargetRef, 364 Conditions: discovery.EndpointConditions{Ready: &trueBool}, 365 }) 366 } 367 368 return &corev1.Endpoints{ 369 ObjectMeta: metav1.ObjectMeta{ 370 Name: name, 371 Namespace: namespace, 372 Labels: map[string]string{ 373 discovery.LabelSkipMirror: "true", 374 }, 375 }, 376 Subsets: []corev1.EndpointSubset{subset}, 377 }, epSlice 378 } 379 380 func TestEndpointManagerEnsureEndpointSliceFromEndpoints(t *testing.T) { 381 endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4"}) 382 endpoints2, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"}) 383 384 testCases := map[string]struct { 385 expectedError error 386 expectedEndpointSlice *discovery.EndpointSlice 387 initialState []runtime.Object 388 namespaceParam string 389 endpointsParam *corev1.Endpoints 390 }{ 391 "existing-endpointslice-no-change": { 392 expectedError: nil, 393 expectedEndpointSlice: epSlice1, 394 initialState: []runtime.Object{epSlice1}, 395 namespaceParam: "testing", 396 endpointsParam: endpoints1, 397 }, 398 "existing-endpointslice-change": { 399 expectedError: nil, 400 expectedEndpointSlice: epSlice2, 401 initialState: []runtime.Object{epSlice1}, 402 namespaceParam: "testing", 403 endpointsParam: endpoints2, 404 }, 405 "missing-endpointslice": { 406 expectedError: nil, 407 expectedEndpointSlice: epSlice1, 408 initialState: []runtime.Object{}, 409 namespaceParam: "testing", 410 endpointsParam: endpoints1, 411 }, 412 } 413 414 for name, testCase := range testCases { 415 t.Run(name, func(t *testing.T) { 416 client := fake.NewSimpleClientset(testCase.initialState...) 417 epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1()) 418 419 err := epAdapter.EnsureEndpointSliceFromEndpoints(testCase.namespaceParam, testCase.endpointsParam) 420 if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) { 421 t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err) 422 } 423 424 endpointSlice, err := client.DiscoveryV1().EndpointSlices(testCase.namespaceParam).Get(context.TODO(), testCase.endpointsParam.Name, metav1.GetOptions{}) 425 if err != nil && !errors.IsNotFound(err) { 426 t.Fatalf("Error getting Endpoint Slice: %v", err) 427 } 428 429 if !apiequality.Semantic.DeepEqual(endpointSlice, testCase.expectedEndpointSlice) { 430 t.Errorf("Expected Endpoint Slice: %v, got: %v", testCase.expectedEndpointSlice, endpointSlice) 431 } 432 }) 433 } 434 }