k8s.io/kubernetes@v1.29.3/pkg/proxy/endpointslicecache_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 proxy 18 19 import ( 20 "fmt" 21 "reflect" 22 "testing" 23 24 v1 "k8s.io/api/core/v1" 25 discovery "k8s.io/api/discovery/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/utils/ptr" 30 ) 31 32 func TestEndpointsMapFromESC(t *testing.T) { 33 testCases := map[string]struct { 34 endpointSlices []*discovery.EndpointSlice 35 hostname string 36 namespacedName types.NamespacedName 37 expectedMap map[ServicePortName][]*BaseEndpointInfo 38 }{ 39 "1 slice, 2 hosts, ports 80,443": { 40 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 41 hostname: "host1", 42 endpointSlices: []*discovery.EndpointSlice{ 43 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](80), ptr.To[int32](443)}), 44 }, 45 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 46 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 47 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 48 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: true, ready: true, serving: true, terminating: false}, 49 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 50 }, 51 makeServicePortName("ns1", "svc1", "port-1", v1.ProtocolTCP): { 52 &BaseEndpointInfo{ip: "10.0.1.1", port: 443, endpoint: "10.0.1.1:443", isLocal: false, ready: true, serving: true, terminating: false}, 53 &BaseEndpointInfo{ip: "10.0.1.2", port: 443, endpoint: "10.0.1.2:443", isLocal: true, ready: true, serving: true, terminating: false}, 54 &BaseEndpointInfo{ip: "10.0.1.3", port: 443, endpoint: "10.0.1.3:443", isLocal: false, ready: true, serving: true, terminating: false}, 55 }, 56 }, 57 }, 58 "2 slices, same port": { 59 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 60 endpointSlices: []*discovery.EndpointSlice{ 61 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 62 generateEndpointSlice("svc1", "ns1", 2, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 63 }, 64 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 65 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 66 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 67 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 68 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 69 &BaseEndpointInfo{ip: "10.0.2.1", port: 80, endpoint: "10.0.2.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 70 &BaseEndpointInfo{ip: "10.0.2.2", port: 80, endpoint: "10.0.2.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 71 &BaseEndpointInfo{ip: "10.0.2.3", port: 80, endpoint: "10.0.2.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 72 }, 73 }, 74 }, 75 // 2 slices, with some overlapping endpoints, result should be a union 76 // of the 2. 77 "2 overlapping slices, same port": { 78 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 79 endpointSlices: []*discovery.EndpointSlice{ 80 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 81 generateEndpointSlice("svc1", "ns1", 1, 4, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 82 }, 83 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 84 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 85 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 86 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 87 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 88 &BaseEndpointInfo{ip: "10.0.1.4", port: 80, endpoint: "10.0.1.4:80", isLocal: false, ready: true, serving: true, terminating: false}, 89 }, 90 }, 91 }, 92 // 2 slices with all endpoints overlapping, more unready in first than 93 // second. If an endpoint is marked ready, we add it to the 94 // EndpointsMap, even if conditions.Ready isn't true for another 95 // matching endpoint 96 "2 slices, overlapping endpoints, some endpoints unready in 1 or both": { 97 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 98 endpointSlices: []*discovery.EndpointSlice{ 99 generateEndpointSlice("svc1", "ns1", 1, 10, 3, 999, []string{}, []*int32{ptr.To[int32](80)}), 100 generateEndpointSlice("svc1", "ns1", 1, 10, 6, 999, []string{}, []*int32{ptr.To[int32](80)}), 101 }, 102 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 103 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 104 &BaseEndpointInfo{ip: "10.0.1.10", port: 80, endpoint: "10.0.1.10:80", isLocal: false, ready: true, serving: true, terminating: false}, 105 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 106 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 107 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 108 &BaseEndpointInfo{ip: "10.0.1.4", port: 80, endpoint: "10.0.1.4:80", isLocal: false, ready: true, serving: true, terminating: false}, 109 &BaseEndpointInfo{ip: "10.0.1.5", port: 80, endpoint: "10.0.1.5:80", isLocal: false, ready: true, serving: true, terminating: false}, 110 &BaseEndpointInfo{ip: "10.0.1.6", port: 80, endpoint: "10.0.1.6:80", isLocal: false, ready: false, serving: false, terminating: false}, 111 &BaseEndpointInfo{ip: "10.0.1.7", port: 80, endpoint: "10.0.1.7:80", isLocal: false, ready: true, serving: true, terminating: false}, 112 &BaseEndpointInfo{ip: "10.0.1.8", port: 80, endpoint: "10.0.1.8:80", isLocal: false, ready: true, serving: true, terminating: false}, 113 &BaseEndpointInfo{ip: "10.0.1.9", port: 80, endpoint: "10.0.1.9:80", isLocal: false, ready: true, serving: true, terminating: false}, 114 }, 115 }, 116 }, 117 // 2 slices with all endpoints overlapping, some unready and terminating. 118 "2 slices, overlapping endpoints, some endpoints unready and some endpoints terminating": { 119 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 120 endpointSlices: []*discovery.EndpointSlice{ 121 generateEndpointSlice("svc1", "ns1", 1, 10, 3, 5, []string{}, []*int32{ptr.To[int32](80)}), 122 generateEndpointSlice("svc1", "ns1", 1, 10, 6, 5, []string{}, []*int32{ptr.To[int32](80)}), 123 }, 124 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 125 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 126 &BaseEndpointInfo{ip: "10.0.1.10", port: 80, endpoint: "10.0.1.10:80", isLocal: false, ready: false, serving: true, terminating: true}, 127 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 128 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 129 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 130 &BaseEndpointInfo{ip: "10.0.1.4", port: 80, endpoint: "10.0.1.4:80", isLocal: false, ready: true, serving: true, terminating: false}, 131 &BaseEndpointInfo{ip: "10.0.1.5", port: 80, endpoint: "10.0.1.5:80", isLocal: false, ready: false, serving: true, terminating: true}, 132 &BaseEndpointInfo{ip: "10.0.1.6", port: 80, endpoint: "10.0.1.6:80", isLocal: false, ready: false, serving: false, terminating: false}, 133 &BaseEndpointInfo{ip: "10.0.1.7", port: 80, endpoint: "10.0.1.7:80", isLocal: false, ready: true, serving: true, terminating: false}, 134 &BaseEndpointInfo{ip: "10.0.1.8", port: 80, endpoint: "10.0.1.8:80", isLocal: false, ready: true, serving: true, terminating: false}, 135 &BaseEndpointInfo{ip: "10.0.1.9", port: 80, endpoint: "10.0.1.9:80", isLocal: false, ready: true, serving: true, terminating: false}, 136 }, 137 }, 138 }, 139 "2 slices, overlapping endpoints, all unready": { 140 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 141 endpointSlices: []*discovery.EndpointSlice{ 142 generateEndpointSlice("svc1", "ns1", 1, 10, 1, 999, []string{}, []*int32{ptr.To[int32](80)}), 143 generateEndpointSlice("svc1", "ns1", 1, 10, 1, 999, []string{}, []*int32{ptr.To[int32](80)}), 144 }, 145 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 146 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 147 &BaseEndpointInfo{ip: "10.0.1.10", port: 80, endpoint: "10.0.1.10:80", isLocal: false, ready: false, serving: false, terminating: false}, 148 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: false, serving: false, terminating: false}, 149 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: false, serving: false, terminating: false}, 150 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: false, serving: false, terminating: false}, 151 &BaseEndpointInfo{ip: "10.0.1.4", port: 80, endpoint: "10.0.1.4:80", isLocal: false, ready: false, serving: false, terminating: false}, 152 &BaseEndpointInfo{ip: "10.0.1.5", port: 80, endpoint: "10.0.1.5:80", isLocal: false, ready: false, serving: false, terminating: false}, 153 &BaseEndpointInfo{ip: "10.0.1.6", port: 80, endpoint: "10.0.1.6:80", isLocal: false, ready: false, serving: false, terminating: false}, 154 &BaseEndpointInfo{ip: "10.0.1.7", port: 80, endpoint: "10.0.1.7:80", isLocal: false, ready: false, serving: false, terminating: false}, 155 &BaseEndpointInfo{ip: "10.0.1.8", port: 80, endpoint: "10.0.1.8:80", isLocal: false, ready: false, serving: false, terminating: false}, 156 &BaseEndpointInfo{ip: "10.0.1.9", port: 80, endpoint: "10.0.1.9:80", isLocal: false, ready: false, serving: false, terminating: false}, 157 }, 158 }, 159 }, 160 "3 slices with different services and namespaces": { 161 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 162 endpointSlices: []*discovery.EndpointSlice{ 163 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 164 generateEndpointSlice("svc2", "ns1", 2, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 165 generateEndpointSlice("svc1", "ns2", 3, 3, 999, 999, []string{}, []*int32{ptr.To[int32](80)}), 166 }, 167 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 168 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 169 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 170 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: false, ready: true, serving: true, terminating: false}, 171 &BaseEndpointInfo{ip: "10.0.1.3", port: 80, endpoint: "10.0.1.3:80", isLocal: false, ready: true, serving: true, terminating: false}, 172 }, 173 }, 174 }, 175 // Ensuring that nil port value will not break things. This will 176 // represent all ports in the future, but that has not been implemented 177 // yet. 178 "Nil port should not break anything": { 179 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 180 hostname: "host1", 181 endpointSlices: []*discovery.EndpointSlice{ 182 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{"host1", "host2"}, []*int32{nil}), 183 }, 184 expectedMap: map[ServicePortName][]*BaseEndpointInfo{}, 185 }, 186 // Make sure that different endpoints with duplicate IPs are returned correctly. 187 "Different endpoints with duplicate IPs should not be filtered": { 188 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 189 hostname: "host1", 190 endpointSlices: []*discovery.EndpointSlice{ 191 generateEndpointSliceWithOffset("svc1", "ns1", 1, 1, 2, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](80)}), 192 generateEndpointSliceWithOffset("svc1", "ns1", 2, 1, 2, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](8080)}), 193 }, 194 expectedMap: map[ServicePortName][]*BaseEndpointInfo{ 195 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 196 &BaseEndpointInfo{ip: "10.0.1.1", port: 80, endpoint: "10.0.1.1:80", isLocal: false, ready: true, serving: true, terminating: false}, 197 &BaseEndpointInfo{ip: "10.0.1.1", port: 8080, endpoint: "10.0.1.1:8080", isLocal: false, ready: true, serving: true, terminating: false}, 198 &BaseEndpointInfo{ip: "10.0.1.2", port: 80, endpoint: "10.0.1.2:80", isLocal: true, ready: true, serving: true, terminating: false}, 199 &BaseEndpointInfo{ip: "10.0.1.2", port: 8080, endpoint: "10.0.1.2:8080", isLocal: true, ready: true, serving: true, terminating: false}, 200 }, 201 }, 202 }, 203 } 204 205 for name, tc := range testCases { 206 t.Run(name, func(t *testing.T) { 207 esCache := NewEndpointSliceCache(tc.hostname, v1.IPv4Protocol, nil, nil) 208 209 cmc := newCacheMutationCheck(tc.endpointSlices) 210 for _, endpointSlice := range tc.endpointSlices { 211 esCache.updatePending(endpointSlice, false) 212 } 213 214 compareEndpointsMapsStr(t, esCache.getEndpointsMap(tc.namespacedName, esCache.trackerByServiceMap[tc.namespacedName].pending), tc.expectedMap) 215 cmc.Check(t) 216 }) 217 } 218 } 219 220 func TestEndpointInfoByServicePort(t *testing.T) { 221 testCases := map[string]struct { 222 namespacedName types.NamespacedName 223 endpointSlices []*discovery.EndpointSlice 224 hostname string 225 expectedMap spToEndpointMap 226 }{ 227 "simple use case with 3 endpoints": { 228 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 229 hostname: "host1", 230 endpointSlices: []*discovery.EndpointSlice{ 231 generateEndpointSlice("svc1", "ns1", 1, 3, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](80)}), 232 }, 233 expectedMap: spToEndpointMap{ 234 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 235 "10.0.1.1:80": &BaseEndpointInfo{ 236 ip: "10.0.1.1", 237 port: 80, 238 endpoint: "10.0.1.1:80", 239 isLocal: false, 240 ready: true, 241 serving: true, 242 terminating: false, 243 }, 244 "10.0.1.2:80": &BaseEndpointInfo{ 245 ip: "10.0.1.2", 246 port: 80, 247 endpoint: "10.0.1.2:80", 248 isLocal: true, 249 ready: true, 250 serving: true, 251 terminating: false, 252 }, 253 "10.0.1.3:80": &BaseEndpointInfo{ 254 ip: "10.0.1.3", 255 port: 80, 256 endpoint: "10.0.1.3:80", 257 isLocal: false, 258 ready: true, 259 serving: true, 260 terminating: false, 261 }, 262 }, 263 }, 264 }, 265 "4 different slices with duplicate IPs": { 266 namespacedName: types.NamespacedName{Name: "svc1", Namespace: "ns1"}, 267 hostname: "host1", 268 endpointSlices: []*discovery.EndpointSlice{ 269 generateEndpointSliceWithOffset("svc1", "ns1", 1, 1, 2, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](80)}), 270 generateEndpointSliceWithOffset("svc1", "ns1", 2, 1, 2, 999, 999, []string{"host1", "host2"}, []*int32{ptr.To[int32](8080)}), 271 }, 272 expectedMap: spToEndpointMap{ 273 makeServicePortName("ns1", "svc1", "port-0", v1.ProtocolTCP): { 274 "10.0.1.1:80": &BaseEndpointInfo{ 275 ip: "10.0.1.1", 276 port: 80, 277 endpoint: "10.0.1.1:80", 278 isLocal: false, 279 ready: true, 280 serving: true, 281 terminating: false, 282 }, 283 "10.0.1.2:80": &BaseEndpointInfo{ 284 ip: "10.0.1.2", 285 port: 80, 286 endpoint: "10.0.1.2:80", 287 isLocal: true, 288 ready: true, 289 serving: true, 290 terminating: false, 291 }, 292 "10.0.1.1:8080": &BaseEndpointInfo{ 293 ip: "10.0.1.1", 294 port: 8080, 295 endpoint: "10.0.1.1:8080", 296 isLocal: false, 297 ready: true, 298 serving: true, 299 terminating: false, 300 }, 301 "10.0.1.2:8080": &BaseEndpointInfo{ 302 ip: "10.0.1.2", 303 port: 8080, 304 endpoint: "10.0.1.2:8080", 305 isLocal: true, 306 ready: true, 307 serving: true, 308 terminating: false, 309 }, 310 }, 311 }, 312 }, 313 } 314 315 for name, tc := range testCases { 316 t.Run(name, func(t *testing.T) { 317 esCache := NewEndpointSliceCache(tc.hostname, v1.IPv4Protocol, nil, nil) 318 319 for _, endpointSlice := range tc.endpointSlices { 320 esCache.updatePending(endpointSlice, false) 321 } 322 323 got := esCache.endpointInfoByServicePort(tc.namespacedName, esCache.trackerByServiceMap[tc.namespacedName].pending) 324 if !reflect.DeepEqual(got, tc.expectedMap) { 325 t.Errorf("endpointInfoByServicePort does not match. Want: %+v, Got: %+v", tc.expectedMap, got) 326 } 327 }) 328 } 329 } 330 331 func TestEsInfoChanged(t *testing.T) { 332 p80 := int32(80) 333 p443 := int32(443) 334 port80 := discovery.EndpointPort{Port: &p80, Name: ptr.To("http"), Protocol: ptr.To(v1.ProtocolTCP)} 335 port443 := discovery.EndpointPort{Port: &p443, Name: ptr.To("https"), Protocol: ptr.To(v1.ProtocolTCP)} 336 endpoint1 := discovery.Endpoint{Addresses: []string{"10.0.1.0"}} 337 endpoint2 := discovery.Endpoint{Addresses: []string{"10.0.1.1"}} 338 339 objMeta := metav1.ObjectMeta{ 340 Name: "foo", 341 Namespace: "bar", 342 Labels: map[string]string{discovery.LabelServiceName: "svc1"}, 343 } 344 345 testCases := map[string]struct { 346 cache *EndpointSliceCache 347 initialSlice *discovery.EndpointSlice 348 updatedSlice *discovery.EndpointSlice 349 expectChanged bool 350 }{ 351 "identical slices, ports only": { 352 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 353 initialSlice: &discovery.EndpointSlice{ 354 ObjectMeta: objMeta, 355 Ports: []discovery.EndpointPort{port80}, 356 }, 357 updatedSlice: &discovery.EndpointSlice{ 358 ObjectMeta: objMeta, 359 Ports: []discovery.EndpointPort{port80}, 360 }, 361 expectChanged: false, 362 }, 363 "identical slices, ports out of order": { 364 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 365 initialSlice: &discovery.EndpointSlice{ 366 ObjectMeta: objMeta, 367 Ports: []discovery.EndpointPort{port443, port80}, 368 }, 369 updatedSlice: &discovery.EndpointSlice{ 370 ObjectMeta: objMeta, 371 Ports: []discovery.EndpointPort{port80, port443}, 372 }, 373 expectChanged: false, 374 }, 375 "port removed": { 376 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 377 initialSlice: &discovery.EndpointSlice{ 378 ObjectMeta: objMeta, 379 Ports: []discovery.EndpointPort{port443, port80}, 380 }, 381 updatedSlice: &discovery.EndpointSlice{ 382 ObjectMeta: objMeta, 383 Ports: []discovery.EndpointPort{port443}, 384 }, 385 expectChanged: true, 386 }, 387 "port added": { 388 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 389 initialSlice: &discovery.EndpointSlice{ 390 ObjectMeta: objMeta, 391 Ports: []discovery.EndpointPort{port443}, 392 }, 393 updatedSlice: &discovery.EndpointSlice{ 394 ObjectMeta: objMeta, 395 Ports: []discovery.EndpointPort{port443, port80}, 396 }, 397 expectChanged: true, 398 }, 399 "identical with endpoints": { 400 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 401 initialSlice: &discovery.EndpointSlice{ 402 ObjectMeta: objMeta, 403 Ports: []discovery.EndpointPort{port443}, 404 Endpoints: []discovery.Endpoint{endpoint1, endpoint2}, 405 }, 406 updatedSlice: &discovery.EndpointSlice{ 407 ObjectMeta: objMeta, 408 Ports: []discovery.EndpointPort{port443}, 409 Endpoints: []discovery.Endpoint{endpoint1, endpoint2}, 410 }, 411 expectChanged: false, 412 }, 413 "identical with endpoints out of order": { 414 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 415 initialSlice: &discovery.EndpointSlice{ 416 ObjectMeta: objMeta, 417 Ports: []discovery.EndpointPort{port443}, 418 Endpoints: []discovery.Endpoint{endpoint1, endpoint2}, 419 }, 420 updatedSlice: &discovery.EndpointSlice{ 421 ObjectMeta: objMeta, 422 Ports: []discovery.EndpointPort{port443}, 423 Endpoints: []discovery.Endpoint{endpoint2, endpoint1}, 424 }, 425 expectChanged: false, 426 }, 427 "identical with endpoint added": { 428 cache: NewEndpointSliceCache("", v1.IPv4Protocol, nil, nil), 429 initialSlice: &discovery.EndpointSlice{ 430 ObjectMeta: objMeta, 431 Ports: []discovery.EndpointPort{port443}, 432 Endpoints: []discovery.Endpoint{endpoint1}, 433 }, 434 updatedSlice: &discovery.EndpointSlice{ 435 ObjectMeta: objMeta, 436 Ports: []discovery.EndpointPort{port443}, 437 Endpoints: []discovery.Endpoint{endpoint2, endpoint1}, 438 }, 439 expectChanged: true, 440 }, 441 } 442 443 for name, tc := range testCases { 444 t.Run(name, func(t *testing.T) { 445 cmc := newCacheMutationCheck([]*discovery.EndpointSlice{tc.initialSlice}) 446 447 if tc.initialSlice != nil { 448 tc.cache.updatePending(tc.initialSlice, false) 449 tc.cache.checkoutChanges() 450 } 451 452 serviceKey, sliceKey, err := endpointSliceCacheKeys(tc.updatedSlice) 453 if err != nil { 454 t.Fatalf("Expected no error calling endpointSliceCacheKeys(): %v", err) 455 } 456 457 esInfo := newEndpointSliceInfo(tc.updatedSlice, false) 458 changed := tc.cache.esInfoChanged(serviceKey, sliceKey, esInfo) 459 460 if tc.expectChanged != changed { 461 t.Errorf("Expected esInfoChanged() to return %t, got %t", tc.expectChanged, changed) 462 } 463 464 cmc.Check(t) 465 }) 466 } 467 } 468 469 func generateEndpointSliceWithOffset(serviceName, namespace string, sliceNum, offset, numEndpoints, unreadyMod int, terminatingMod int, hosts []string, portNums []*int32) *discovery.EndpointSlice { 470 endpointSlice := &discovery.EndpointSlice{ 471 ObjectMeta: metav1.ObjectMeta{ 472 Name: fmt.Sprintf("%s-%d", serviceName, sliceNum), 473 Namespace: namespace, 474 Labels: map[string]string{discovery.LabelServiceName: serviceName}, 475 }, 476 Ports: []discovery.EndpointPort{}, 477 AddressType: discovery.AddressTypeIPv4, 478 Endpoints: []discovery.Endpoint{}, 479 } 480 481 for i, portNum := range portNums { 482 endpointSlice.Ports = append(endpointSlice.Ports, discovery.EndpointPort{ 483 Name: ptr.To(fmt.Sprintf("port-%d", i)), 484 Port: portNum, 485 Protocol: ptr.To(v1.ProtocolTCP), 486 }) 487 } 488 489 for i := 1; i <= numEndpoints; i++ { 490 readyCondition := i%unreadyMod != 0 491 terminatingCondition := i%terminatingMod == 0 492 493 ready := ptr.To(readyCondition && !terminatingCondition) 494 serving := ptr.To(readyCondition) 495 terminating := ptr.To(terminatingCondition) 496 497 endpoint := discovery.Endpoint{ 498 Addresses: []string{fmt.Sprintf("10.0.%d.%d", offset, i)}, 499 Conditions: discovery.EndpointConditions{ 500 Ready: ready, 501 Serving: serving, 502 Terminating: terminating, 503 }, 504 } 505 506 if len(hosts) > 0 { 507 hostname := hosts[i%len(hosts)] 508 endpoint.NodeName = &hostname 509 } 510 511 endpointSlice.Endpoints = append(endpointSlice.Endpoints, endpoint) 512 } 513 514 return endpointSlice 515 } 516 517 func generateEndpointSlice(serviceName, namespace string, sliceNum, numEndpoints, unreadyMod int, terminatingMod int, hosts []string, portNums []*int32) *discovery.EndpointSlice { 518 return generateEndpointSliceWithOffset(serviceName, namespace, sliceNum, sliceNum, numEndpoints, unreadyMod, terminatingMod, hosts, portNums) 519 } 520 521 // cacheMutationCheck helps ensure that cached objects have not been changed 522 // in any way throughout a test run. 523 type cacheMutationCheck struct { 524 objects []cacheObject 525 } 526 527 // cacheObject stores a reference to an original object as well as a deep copy 528 // of that object to track any mutations in the original object. 529 type cacheObject struct { 530 original runtime.Object 531 deepCopy runtime.Object 532 } 533 534 // newCacheMutationCheck initializes a cacheMutationCheck with EndpointSlices. 535 func newCacheMutationCheck(endpointSlices []*discovery.EndpointSlice) cacheMutationCheck { 536 cmc := cacheMutationCheck{} 537 for _, endpointSlice := range endpointSlices { 538 cmc.Add(endpointSlice) 539 } 540 return cmc 541 } 542 543 // Add appends a runtime.Object and a deep copy of that object into the 544 // cacheMutationCheck. 545 func (cmc *cacheMutationCheck) Add(o runtime.Object) { 546 cmc.objects = append(cmc.objects, cacheObject{ 547 original: o, 548 deepCopy: o.DeepCopyObject(), 549 }) 550 } 551 552 // Check verifies that no objects in the cacheMutationCheck have been mutated. 553 func (cmc *cacheMutationCheck) Check(t *testing.T) { 554 for _, o := range cmc.objects { 555 if !reflect.DeepEqual(o.original, o.deepCopy) { 556 // Cached objects can't be safely mutated and instead should be deep 557 // copied before changed in any way. 558 t.Errorf("Cached object was unexpectedly mutated. Original: %+v, Mutated: %+v", o.deepCopy, o.original) 559 } 560 } 561 }