
Since https://github.com/kubernetes/kubernetes/pull/112648, we can efficiently handle selectors from pre-existing `map[string]string`, making the cache obsolete. Benchmark: ``` name old time/op new time/op delta GetPodServiceMemberships-48 189µs ± 1% 193µs ± 1% +2.10% (p=0.000 n=10+10) name old alloc/op new alloc/op delta GetPodServiceMemberships-48 59.0kB ± 0% 58.9kB ± 0% -0.09% (p=0.000 n=9+9) name old allocs/op new allocs/op delta GetPodServiceMemberships-48 1.02k ± 0% 1.02k ± 0% ~ (all equal) ```
305 lines
10 KiB
Go
305 lines
10 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package endpoint
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
discovery "k8s.io/api/discovery/v1"
|
|
"k8s.io/apimachinery/pkg/conversion"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
v1listers "k8s.io/client-go/listers/core/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
|
"k8s.io/kubernetes/pkg/controller"
|
|
"k8s.io/kubernetes/pkg/util/hash"
|
|
)
|
|
|
|
// semanticIgnoreResourceVersion does semantic deep equality checks for objects
|
|
// but excludes ResourceVersion of ObjectReference. They are used when comparing
|
|
// endpoints in Endpoints and EndpointSlice objects to avoid unnecessary updates
|
|
// caused by Pod resourceVersion change.
|
|
var semanticIgnoreResourceVersion = conversion.EqualitiesOrDie(
|
|
func(a, b v1.ObjectReference) bool {
|
|
a.ResourceVersion = ""
|
|
b.ResourceVersion = ""
|
|
return a == b
|
|
},
|
|
)
|
|
|
|
// GetPodServiceMemberships returns a set of Service keys for Services that have
|
|
// a selector matching the given pod.
|
|
func GetPodServiceMemberships(serviceLister v1listers.ServiceLister, pod *v1.Pod) (sets.String, error) {
|
|
set := sets.String{}
|
|
services, err := serviceLister.Services(pod.Namespace).List(labels.Everything())
|
|
if err != nil {
|
|
return set, err
|
|
}
|
|
|
|
for _, service := range services {
|
|
if service.Spec.Selector == nil {
|
|
// if the service has a nil selector this means selectors match nothing, not everything.
|
|
continue
|
|
}
|
|
key, err := controller.KeyFunc(service)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if labels.ValidatedSetSelector(service.Spec.Selector).Matches(labels.Set(pod.Labels)) {
|
|
set.Insert(key)
|
|
}
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
// PortMapKey is used to uniquely identify groups of endpoint ports.
|
|
type PortMapKey string
|
|
|
|
// NewPortMapKey generates a PortMapKey from endpoint ports.
|
|
func NewPortMapKey(endpointPorts []discovery.EndpointPort) PortMapKey {
|
|
sort.Sort(portsInOrder(endpointPorts))
|
|
return PortMapKey(DeepHashObjectToString(endpointPorts))
|
|
}
|
|
|
|
// DeepHashObjectToString creates a unique hash string from a go object.
|
|
func DeepHashObjectToString(objectToWrite interface{}) string {
|
|
hasher := md5.New()
|
|
hash.DeepHashObject(hasher, objectToWrite)
|
|
return hex.EncodeToString(hasher.Sum(nil)[0:])
|
|
}
|
|
|
|
// ShouldPodBeInEndpoints returns true if a specified pod should be in an
|
|
// Endpoints or EndpointSlice resource. Terminating pods are only included if
|
|
// includeTerminating is true.
|
|
func ShouldPodBeInEndpoints(pod *v1.Pod, includeTerminating bool) bool {
|
|
// "Terminal" describes when a Pod is complete (in a succeeded or failed phase).
|
|
// This is distinct from the "Terminating" condition which represents when a Pod
|
|
// is being terminated (metadata.deletionTimestamp is non nil).
|
|
if podutil.IsPodTerminal(pod) {
|
|
return false
|
|
}
|
|
|
|
if len(pod.Status.PodIP) == 0 && len(pod.Status.PodIPs) == 0 {
|
|
return false
|
|
}
|
|
|
|
if !includeTerminating && pod.DeletionTimestamp != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ShouldSetHostname returns true if the Hostname attribute should be set on an
|
|
// Endpoints Address or EndpointSlice Endpoint.
|
|
func ShouldSetHostname(pod *v1.Pod, svc *v1.Service) bool {
|
|
return len(pod.Spec.Hostname) > 0 && pod.Spec.Subdomain == svc.Name && svc.Namespace == pod.Namespace
|
|
}
|
|
|
|
// podEndpointsChanged returns two boolean values. The first is true if the pod has
|
|
// changed in a way that may change existing endpoints. The second value is true if the
|
|
// pod has changed in a way that may affect which Services it matches.
|
|
func podEndpointsChanged(oldPod, newPod *v1.Pod) (bool, bool) {
|
|
// Check if the pod labels have changed, indicating a possible
|
|
// change in the service membership
|
|
labelsChanged := false
|
|
if !reflect.DeepEqual(newPod.Labels, oldPod.Labels) ||
|
|
!hostNameAndDomainAreEqual(newPod, oldPod) {
|
|
labelsChanged = true
|
|
}
|
|
|
|
// If the pod's deletion timestamp is set, remove endpoint from ready address.
|
|
if newPod.DeletionTimestamp != oldPod.DeletionTimestamp {
|
|
return true, labelsChanged
|
|
}
|
|
// If the pod's readiness has changed, the associated endpoint address
|
|
// will move from the unready endpoints set to the ready endpoints.
|
|
// So for the purposes of an endpoint, a readiness change on a pod
|
|
// means we have a changed pod.
|
|
if podutil.IsPodReady(oldPod) != podutil.IsPodReady(newPod) {
|
|
return true, labelsChanged
|
|
}
|
|
|
|
// Check if the pod IPs have changed
|
|
if len(oldPod.Status.PodIPs) != len(newPod.Status.PodIPs) {
|
|
return true, labelsChanged
|
|
}
|
|
for i := range oldPod.Status.PodIPs {
|
|
if oldPod.Status.PodIPs[i].IP != newPod.Status.PodIPs[i].IP {
|
|
return true, labelsChanged
|
|
}
|
|
}
|
|
|
|
// Endpoints may also reference a pod's Name, Namespace, UID, and NodeName, but
|
|
// the first three are immutable, and NodeName is immutable once initially set,
|
|
// which happens before the pod gets an IP.
|
|
|
|
return false, labelsChanged
|
|
}
|
|
|
|
// GetServicesToUpdateOnPodChange returns a set of Service keys for Services
|
|
// that have potentially been affected by a change to this pod.
|
|
func GetServicesToUpdateOnPodChange(serviceLister v1listers.ServiceLister, old, cur interface{}) sets.String {
|
|
newPod := cur.(*v1.Pod)
|
|
oldPod := old.(*v1.Pod)
|
|
if newPod.ResourceVersion == oldPod.ResourceVersion {
|
|
// Periodic resync will send update events for all known pods.
|
|
// Two different versions of the same pod will always have different RVs
|
|
return sets.String{}
|
|
}
|
|
|
|
podChanged, labelsChanged := podEndpointsChanged(oldPod, newPod)
|
|
|
|
// If both the pod and labels are unchanged, no update is needed
|
|
if !podChanged && !labelsChanged {
|
|
return sets.String{}
|
|
}
|
|
|
|
services, err := GetPodServiceMemberships(serviceLister, newPod)
|
|
if err != nil {
|
|
utilruntime.HandleError(fmt.Errorf("unable to get pod %s/%s's service memberships: %v", newPod.Namespace, newPod.Name, err))
|
|
return sets.String{}
|
|
}
|
|
|
|
if labelsChanged {
|
|
oldServices, err := GetPodServiceMemberships(serviceLister, oldPod)
|
|
if err != nil {
|
|
utilruntime.HandleError(fmt.Errorf("unable to get pod %s/%s's service memberships: %v", newPod.Namespace, newPod.Name, err))
|
|
}
|
|
services = determineNeededServiceUpdates(oldServices, services, podChanged)
|
|
}
|
|
|
|
return services
|
|
}
|
|
|
|
// GetPodFromDeleteAction returns a pointer to a pod if one can be derived from
|
|
// obj (could be a *v1.Pod, or a DeletionFinalStateUnknown marker item).
|
|
func GetPodFromDeleteAction(obj interface{}) *v1.Pod {
|
|
if pod, ok := obj.(*v1.Pod); ok {
|
|
// Enqueue all the services that the pod used to be a member of.
|
|
// This is the same thing we do when we add a pod.
|
|
return pod
|
|
}
|
|
// If we reached here it means the pod was deleted but its final state is unrecorded.
|
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
|
if !ok {
|
|
utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
|
|
return nil
|
|
}
|
|
pod, ok := tombstone.Obj.(*v1.Pod)
|
|
if !ok {
|
|
utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Pod: %#v", obj))
|
|
return nil
|
|
}
|
|
return pod
|
|
}
|
|
|
|
func hostNameAndDomainAreEqual(pod1, pod2 *v1.Pod) bool {
|
|
return pod1.Spec.Hostname == pod2.Spec.Hostname &&
|
|
pod1.Spec.Subdomain == pod2.Spec.Subdomain
|
|
}
|
|
|
|
func determineNeededServiceUpdates(oldServices, services sets.String, podChanged bool) sets.String {
|
|
if podChanged {
|
|
// if the labels and pod changed, all services need to be updated
|
|
services = services.Union(oldServices)
|
|
} else {
|
|
// if only the labels changed, services not common to both the new
|
|
// and old service set (the disjuntive union) need to be updated
|
|
services = services.Difference(oldServices).Union(oldServices.Difference(services))
|
|
}
|
|
return services
|
|
}
|
|
|
|
// portsInOrder helps sort endpoint ports in a consistent way for hashing.
|
|
type portsInOrder []discovery.EndpointPort
|
|
|
|
func (sl portsInOrder) Len() int { return len(sl) }
|
|
func (sl portsInOrder) Swap(i, j int) { sl[i], sl[j] = sl[j], sl[i] }
|
|
func (sl portsInOrder) Less(i, j int) bool {
|
|
h1 := DeepHashObjectToString(sl[i])
|
|
h2 := DeepHashObjectToString(sl[j])
|
|
return h1 < h2
|
|
}
|
|
|
|
// EndpointsEqualBeyondHash returns true if endpoints have equal attributes
|
|
// but excludes equality checks that would have already been covered with
|
|
// endpoint hashing (see hashEndpoint func for more info) and ignores difference
|
|
// in ResourceVersion of TargetRef.
|
|
func EndpointsEqualBeyondHash(ep1, ep2 *discovery.Endpoint) bool {
|
|
if stringPtrChanged(ep1.NodeName, ep2.NodeName) {
|
|
return false
|
|
}
|
|
|
|
if stringPtrChanged(ep1.Zone, ep2.Zone) {
|
|
return false
|
|
}
|
|
|
|
if boolPtrChanged(ep1.Conditions.Ready, ep2.Conditions.Ready) {
|
|
return false
|
|
}
|
|
|
|
if boolPtrChanged(ep1.Conditions.Serving, ep2.Conditions.Serving) {
|
|
return false
|
|
}
|
|
|
|
if boolPtrChanged(ep1.Conditions.Terminating, ep2.Conditions.Terminating) {
|
|
return false
|
|
}
|
|
|
|
if !semanticIgnoreResourceVersion.DeepEqual(ep1.TargetRef, ep2.TargetRef) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// boolPtrChanged returns true if a set of bool pointers have different values.
|
|
func boolPtrChanged(ptr1, ptr2 *bool) bool {
|
|
if (ptr1 == nil) != (ptr2 == nil) {
|
|
return true
|
|
}
|
|
if ptr1 != nil && ptr2 != nil && *ptr1 != *ptr2 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// stringPtrChanged returns true if a set of string pointers have different values.
|
|
func stringPtrChanged(ptr1, ptr2 *string) bool {
|
|
if (ptr1 == nil) != (ptr2 == nil) {
|
|
return true
|
|
}
|
|
if ptr1 != nil && ptr2 != nil && *ptr1 != *ptr2 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EndpointSubsetsEqualIgnoreResourceVersion returns true if EndpointSubsets
|
|
// have equal attributes but excludes ResourceVersion of Pod.
|
|
func EndpointSubsetsEqualIgnoreResourceVersion(subsets1, subsets2 []v1.EndpointSubset) bool {
|
|
return semanticIgnoreResourceVersion.DeepEqual(subsets1, subsets2)
|
|
}
|