kube-router/pkg/utils/service.go
2025-12-02 18:22:37 -06:00

131 lines
4.6 KiB
Go

package utils
import (
"fmt"
"strings"
v1core "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
const (
IPInIPHeaderLength = 20
)
// ServiceNameforEndpointSlice returns the name of the service that created the EndpointSlice for a given EndpointSlice
//
// With endpoints, the name of the endpoint object always matches the service object, however when it comes to
// EndpointSlices, things work a bit different as k8s' controller will autogenerated it (something like: foo-kl29b)
//
// We can get service information from a number of spots:
// * From the ownerReferences in the metadata EndpointSlice -> metadata -> ownerReferences[0] -> name
// * We can also get this from the label: kubernetes.io/service-name
// * generateName will also contain the prefix for the autogenerated name which should align with our service name
//
// We'll all through all of these and do our best to identify the service's name, if we aren't able to find any of these
// or they disagree with each other we'll throw an error
func ServiceNameforEndpointSlice(es *discoveryv1.EndpointSlice) (string, error) {
const serviceNameLabel = "kubernetes.io/service-name"
var ownerRefName, labelSvcName, generateName, finalSvcName string
ownerRef := es.GetObjectMeta().GetOwnerReferences()
if len(ownerRef) == 1 {
ownerRefName = ownerRef[0].Name
}
labels := es.GetObjectMeta().GetLabels()
if svcLabel, ok := labels[serviceNameLabel]; ok {
labelSvcName = svcLabel
}
if es.GetObjectMeta().GetGenerateName() != "" {
generateName = strings.TrimRight(es.GetObjectMeta().GetGenerateName(), "-")
}
if ownerRefName == "" && labelSvcName == "" && generateName == "" {
return "", fmt.Errorf("all identifiers for service are empty on this EndpointSlice, unable to determine "+
"owning service for: %s/%s", es.Namespace, es.Name)
}
// Take things in an order of precedence here: generateName < ownerRefName < labelSvcName
finalSvcName = generateName
if ownerRefName != "" {
finalSvcName = ownerRefName
}
if labelSvcName != "" {
finalSvcName = labelSvcName
}
// At this point we do some checks to ensure that the final owning service name is sane. Specifically, we want to
// check it against labelSvcName and ownerRefName if they were not blank and log a debug log if they don't agree. We
// don't worry about generateName as that is less conclusive.
//
// See: https://github.com/cloudnativelabs/kube-router/issues/1957 for more information
if ownerRefName != "" && finalSvcName != ownerRefName {
klog.V(1).Infof("The metadata ownerReference name %s and the label service name (%s) %s don't appear to "+
"match for EndpointSlice %s/%s. In this case we prefer the label service name.",
ownerRefName, serviceNameLabel, labelSvcName, es.Namespace, es.Name)
}
return finalSvcName, nil
}
// ServiceForEndpoints given EndpointSlice object return Service API object if it exists
func ServiceForEndpointSlice(ci *cache.Indexer, es *discoveryv1.EndpointSlice) (any, bool, error) {
svcName, err := ServiceNameforEndpointSlice(es)
if err != nil {
return nil, false, err
}
// The key that we're looking for here is just <namespace>/<name>
key := fmt.Sprintf("%s/%s", es.Namespace, svcName)
klog.V(2).Infof("key for looking up service from EndpointSlice is: %s", key)
item, exists, err := (*ci).GetByKey(key)
if err != nil {
return nil, false, err
}
if !exists {
return nil, false, nil
}
return item, true, nil
}
// ServiceHasNoClusterIP decides whether or not the this service is a headless service which is often useful to
// kube-router as there is no need to execute logic on most headless changes. Function takes a generic interface as its
// input parameter so that it can be used more easily in early processing if needed. If a non-service object is given,
// function will return false.
func ServiceHasNoClusterIP(obj any) bool {
if svc, _ := obj.(*v1core.Service); svc != nil {
if svc.Spec.Type == v1core.ServiceTypeClusterIP {
if ClusterIPIsNone(svc.Spec.ClusterIP) && containsOnlyNone(svc.Spec.ClusterIPs) {
return true
}
}
}
return false
}
// ClusterIPIsNone checks to see whether the ClusterIP contains "None" which would indicate that it is headless
func ClusterIPIsNone(clusterIP string) bool {
return strings.ToLower(clusterIP) == "none"
}
// ClusterIPIsNoneOrBlank checks to see whether the ClusterIP contains "None" or is blank
func ClusterIPIsNoneOrBlank(clusterIP string) bool {
return ClusterIPIsNone(clusterIP) || clusterIP == ""
}
func containsOnlyNone(clusterIPs []string) bool {
for _, clusterIP := range clusterIPs {
if !ClusterIPIsNone(clusterIP) {
return false
}
}
return true
}