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