k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/proxy/endpointslicecache.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 "sort" 23 "sync" 24 25 v1 "k8s.io/api/core/v1" 26 discovery "k8s.io/api/discovery/v1" 27 "k8s.io/apimachinery/pkg/types" 28 "k8s.io/apimachinery/pkg/util/sets" 29 utilfeature "k8s.io/apiserver/pkg/util/feature" 30 "k8s.io/client-go/tools/events" 31 "k8s.io/klog/v2" 32 "k8s.io/kubernetes/pkg/features" 33 proxyutil "k8s.io/kubernetes/pkg/proxy/util" 34 utilnet "k8s.io/utils/net" 35 ) 36 37 // EndpointSliceCache is used as a cache of EndpointSlice information. 38 type EndpointSliceCache struct { 39 // lock protects trackerByServiceMap. 40 lock sync.Mutex 41 42 // trackerByServiceMap is the basis of this cache. It contains endpoint 43 // slice trackers grouped by service name and endpoint slice name. The first 44 // key represents a namespaced service name while the second key represents 45 // an endpoint slice name. Since endpoints can move between slices, we 46 // require slice specific caching to prevent endpoints being removed from 47 // the cache when they may have just moved to a different slice. 48 trackerByServiceMap map[types.NamespacedName]*endpointSliceTracker 49 50 makeEndpointInfo makeEndpointFunc 51 hostname string 52 ipFamily v1.IPFamily 53 recorder events.EventRecorder 54 } 55 56 // endpointSliceTracker keeps track of EndpointSlices as they have been applied 57 // by a proxier along with any pending EndpointSlices that have been updated 58 // in this cache but not yet applied by a proxier. 59 type endpointSliceTracker struct { 60 applied endpointSliceDataByName 61 pending endpointSliceDataByName 62 } 63 64 // endpointSliceDataByName groups endpointSliceData by the names of the 65 // corresponding EndpointSlices. 66 type endpointSliceDataByName map[string]*endpointSliceData 67 68 // endpointSliceData contains information about a single EndpointSlice update or removal. 69 type endpointSliceData struct { 70 endpointSlice *discovery.EndpointSlice 71 remove bool 72 } 73 74 // NewEndpointSliceCache initializes an EndpointSliceCache. 75 func NewEndpointSliceCache(hostname string, ipFamily v1.IPFamily, recorder events.EventRecorder, makeEndpointInfo makeEndpointFunc) *EndpointSliceCache { 76 if makeEndpointInfo == nil { 77 makeEndpointInfo = standardEndpointInfo 78 } 79 return &EndpointSliceCache{ 80 trackerByServiceMap: map[types.NamespacedName]*endpointSliceTracker{}, 81 hostname: hostname, 82 ipFamily: ipFamily, 83 makeEndpointInfo: makeEndpointInfo, 84 recorder: recorder, 85 } 86 } 87 88 // newEndpointSliceTracker initializes an endpointSliceTracker. 89 func newEndpointSliceTracker() *endpointSliceTracker { 90 return &endpointSliceTracker{ 91 applied: endpointSliceDataByName{}, 92 pending: endpointSliceDataByName{}, 93 } 94 } 95 96 // standardEndpointInfo is the default makeEndpointFunc. 97 func standardEndpointInfo(ep *BaseEndpointInfo, _ *ServicePortName) Endpoint { 98 return ep 99 } 100 101 // updatePending updates a pending slice in the cache. 102 func (cache *EndpointSliceCache) updatePending(endpointSlice *discovery.EndpointSlice, remove bool) bool { 103 serviceKey, sliceKey, err := endpointSliceCacheKeys(endpointSlice) 104 if err != nil { 105 klog.ErrorS(err, "Error getting endpoint slice cache keys") 106 return false 107 } 108 109 esData := &endpointSliceData{endpointSlice, remove} 110 111 cache.lock.Lock() 112 defer cache.lock.Unlock() 113 114 if _, ok := cache.trackerByServiceMap[serviceKey]; !ok { 115 cache.trackerByServiceMap[serviceKey] = newEndpointSliceTracker() 116 } 117 118 changed := cache.esDataChanged(serviceKey, sliceKey, esData) 119 120 if changed { 121 cache.trackerByServiceMap[serviceKey].pending[sliceKey] = esData 122 } 123 124 return changed 125 } 126 127 // checkoutChanges returns a map of all endpointsChanges that are 128 // pending and then marks them as applied. 129 func (cache *EndpointSliceCache) checkoutChanges() map[types.NamespacedName]*endpointsChange { 130 changes := make(map[types.NamespacedName]*endpointsChange) 131 132 cache.lock.Lock() 133 defer cache.lock.Unlock() 134 135 for serviceNN, esTracker := range cache.trackerByServiceMap { 136 if len(esTracker.pending) == 0 { 137 continue 138 } 139 140 change := &endpointsChange{} 141 142 change.previous = cache.getEndpointsMap(serviceNN, esTracker.applied) 143 144 for name, sliceData := range esTracker.pending { 145 if sliceData.remove { 146 delete(esTracker.applied, name) 147 } else { 148 esTracker.applied[name] = sliceData 149 } 150 151 delete(esTracker.pending, name) 152 } 153 154 change.current = cache.getEndpointsMap(serviceNN, esTracker.applied) 155 changes[serviceNN] = change 156 } 157 158 return changes 159 } 160 161 // spToEndpointMap stores groups Endpoint objects by ServicePortName and 162 // endpoint string (returned by Endpoint.String()). 163 type spToEndpointMap map[ServicePortName]map[string]Endpoint 164 165 // getEndpointsMap computes an EndpointsMap for a given set of EndpointSlices. 166 func (cache *EndpointSliceCache) getEndpointsMap(serviceNN types.NamespacedName, sliceDataByName endpointSliceDataByName) EndpointsMap { 167 endpointInfoBySP := cache.endpointInfoByServicePort(serviceNN, sliceDataByName) 168 return endpointsMapFromEndpointInfo(endpointInfoBySP) 169 } 170 171 // endpointInfoByServicePort groups endpoint info by service port name and address. 172 func (cache *EndpointSliceCache) endpointInfoByServicePort(serviceNN types.NamespacedName, sliceDataByName endpointSliceDataByName) spToEndpointMap { 173 endpointInfoBySP := spToEndpointMap{} 174 175 for _, sliceData := range sliceDataByName { 176 for _, port := range sliceData.endpointSlice.Ports { 177 if port.Name == nil { 178 klog.ErrorS(nil, "Ignoring port with nil name", "portName", port.Name) 179 continue 180 } 181 // TODO: handle nil ports to mean "all" 182 if port.Port == nil || *port.Port == int32(0) { 183 klog.ErrorS(nil, "Ignoring invalid endpoint port", "portName", *port.Name) 184 continue 185 } 186 187 svcPortName := ServicePortName{ 188 NamespacedName: serviceNN, 189 Port: *port.Name, 190 Protocol: *port.Protocol, 191 } 192 193 endpointInfoBySP[svcPortName] = cache.addEndpoints(&svcPortName, int(*port.Port), endpointInfoBySP[svcPortName], sliceData.endpointSlice.Endpoints) 194 } 195 } 196 197 return endpointInfoBySP 198 } 199 200 // addEndpoints adds an Endpoint for each unique endpoint. 201 func (cache *EndpointSliceCache) addEndpoints(svcPortName *ServicePortName, portNum int, endpointSet map[string]Endpoint, endpoints []discovery.Endpoint) map[string]Endpoint { 202 if endpointSet == nil { 203 endpointSet = map[string]Endpoint{} 204 } 205 206 // iterate through endpoints to add them to endpointSet. 207 for _, endpoint := range endpoints { 208 if len(endpoint.Addresses) == 0 { 209 klog.ErrorS(nil, "Ignoring invalid endpoint port with empty address", "endpoint", endpoint) 210 continue 211 } 212 213 // Filter out the incorrect IP version case. Any endpoint port that 214 // contains incorrect IP version will be ignored. 215 if (cache.ipFamily == v1.IPv6Protocol) != utilnet.IsIPv6String(endpoint.Addresses[0]) { 216 // Emit event on the corresponding service which had a different IP 217 // version than the endpoint. 218 proxyutil.LogAndEmitIncorrectIPVersionEvent(cache.recorder, "endpointslice", endpoint.Addresses[0], svcPortName.NamespacedName.Namespace, svcPortName.NamespacedName.Name, "") 219 continue 220 } 221 222 isLocal := endpoint.NodeName != nil && cache.isLocal(*endpoint.NodeName) 223 224 ready := endpoint.Conditions.Ready == nil || *endpoint.Conditions.Ready 225 serving := endpoint.Conditions.Serving == nil || *endpoint.Conditions.Serving 226 terminating := endpoint.Conditions.Terminating != nil && *endpoint.Conditions.Terminating 227 228 var zoneHints sets.Set[string] 229 if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) { 230 if endpoint.Hints != nil && len(endpoint.Hints.ForZones) > 0 { 231 zoneHints = sets.New[string]() 232 for _, zone := range endpoint.Hints.ForZones { 233 zoneHints.Insert(zone.Name) 234 } 235 } 236 } 237 238 endpointInfo := newBaseEndpointInfo(endpoint.Addresses[0], portNum, isLocal, 239 ready, serving, terminating, zoneHints) 240 241 // This logic ensures we're deduplicating potential overlapping endpoints 242 // isLocal should not vary between matching endpoints, but if it does, we 243 // favor a true value here if it exists. 244 if _, exists := endpointSet[endpointInfo.String()]; !exists || isLocal { 245 endpointSet[endpointInfo.String()] = cache.makeEndpointInfo(endpointInfo, svcPortName) 246 } 247 } 248 249 return endpointSet 250 } 251 252 func (cache *EndpointSliceCache) isLocal(hostname string) bool { 253 return len(cache.hostname) > 0 && hostname == cache.hostname 254 } 255 256 // esDataChanged returns true if the esData parameter should be set as a new 257 // pending value in the cache. 258 func (cache *EndpointSliceCache) esDataChanged(serviceKey types.NamespacedName, sliceKey string, esData *endpointSliceData) bool { 259 if _, ok := cache.trackerByServiceMap[serviceKey]; ok { 260 appliedData, appliedOk := cache.trackerByServiceMap[serviceKey].applied[sliceKey] 261 pendingData, pendingOk := cache.trackerByServiceMap[serviceKey].pending[sliceKey] 262 263 // If there's already a pending value, return whether or not this would 264 // change that. 265 if pendingOk { 266 return !reflect.DeepEqual(esData, pendingData) 267 } 268 269 // If there's already an applied value, return whether or not this would 270 // change that. 271 if appliedOk { 272 return !reflect.DeepEqual(esData, appliedData) 273 } 274 } 275 276 // If this is marked for removal and does not exist in the cache, no changes 277 // are necessary. 278 if esData.remove { 279 return false 280 } 281 282 // If not in the cache, and not marked for removal, it should be added. 283 return true 284 } 285 286 // endpointsMapFromEndpointInfo computes an endpointsMap from endpointInfo that 287 // has been grouped by service port and IP. 288 func endpointsMapFromEndpointInfo(endpointInfoBySP map[ServicePortName]map[string]Endpoint) EndpointsMap { 289 endpointsMap := EndpointsMap{} 290 291 // transform endpointInfoByServicePort into an endpointsMap with sorted IPs. 292 for svcPortName, endpointSet := range endpointInfoBySP { 293 if len(endpointSet) > 0 { 294 endpointsMap[svcPortName] = []Endpoint{} 295 for _, endpointInfo := range endpointSet { 296 endpointsMap[svcPortName] = append(endpointsMap[svcPortName], endpointInfo) 297 298 } 299 // Ensure endpoints are always returned in the same order to simplify diffing. 300 sort.Sort(byEndpoint(endpointsMap[svcPortName])) 301 302 klog.V(3).InfoS("Setting endpoints for service port name", "portName", svcPortName, "endpoints", formatEndpointsList(endpointsMap[svcPortName])) 303 } 304 } 305 306 return endpointsMap 307 } 308 309 // formatEndpointsList returns a string list converted from an endpoints list. 310 func formatEndpointsList(endpoints []Endpoint) []string { 311 var formattedList []string 312 for _, ep := range endpoints { 313 formattedList = append(formattedList, ep.String()) 314 } 315 return formattedList 316 } 317 318 // endpointSliceCacheKeys returns cache keys used for a given EndpointSlice. 319 func endpointSliceCacheKeys(endpointSlice *discovery.EndpointSlice) (types.NamespacedName, string, error) { 320 var err error 321 serviceName, ok := endpointSlice.Labels[discovery.LabelServiceName] 322 if !ok || serviceName == "" { 323 err = fmt.Errorf("no %s label set on endpoint slice: %s", discovery.LabelServiceName, endpointSlice.Name) 324 } else if endpointSlice.Namespace == "" || endpointSlice.Name == "" { 325 err = fmt.Errorf("expected EndpointSlice name and namespace to be set: %v", endpointSlice) 326 } 327 return types.NamespacedName{Namespace: endpointSlice.Namespace, Name: serviceName}, endpointSlice.Name, err 328 } 329 330 // byEndpoint helps sort endpoints by endpoint string. 331 type byEndpoint []Endpoint 332 333 func (e byEndpoint) Len() int { 334 return len(e) 335 } 336 func (e byEndpoint) Swap(i, j int) { 337 e[i], e[j] = e[j], e[i] 338 } 339 func (e byEndpoint) Less(i, j int) bool { 340 return e[i].String() < e[j].String() 341 }