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  }