kube-router/pkg/utils/service.go
Aaron U'Ren da73dea69b feat(NSC): use EndpointSlice instead of Endpoints
With the advent of IPv6 integrated into the NSC we no longer get all IPs
from endpoints, but rather just the primary IP of the pod (which is
often, but not always the IPv4 address).

In order to get all possible endpoint addresses for a given service we
need to switch to using EndpointSlice which also nicely groups addresses
into IPv4 and IPv6 by AddressType and also gives us more information
about the endpoint status by giving us attributes for serving and
terminating, instead of just ready or not ready.

This does mean that users will need to add another permission to their
RBAC in order for kube-router to access these objects.
2023-10-07 08:52:31 -05:00

151 lines
5.1 KiB
Go

package utils
import (
"fmt"
"strings"
v1core "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
const (
IPInIPHeaderLength = 20
)
// ServiceForEndpoints given Endpoint object return Service API object if it exists
func ServiceForEndpoints(ci *cache.Indexer, ep *v1core.Endpoints) (interface{}, bool, error) {
key, err := cache.MetaNamespaceKeyFunc(ep)
if err != nil {
return nil, false, err
}
klog.V(2).Infof("key for looking up service from Endpoint 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
}
// 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 *discovery.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 return error if they don't agree. We
// don't worry about generateName as that is less conclusive.
//
// From above, we already know that if labelSvcName was not blank then it is equal to finalSvcName, so we only need
// to worry about ownerRefName
if ownerRefName != "" && finalSvcName != ownerRefName {
return "", fmt.Errorf("the ownerReferences field on EndpointSlice (%s) doesn't agree with with the %s label "+
"(%s) for %s/%s EndpointSlice", 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 *discovery.EndpointSlice) (interface{}, 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
}
// ServiceIsHeadless 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 ServiceIsHeadless(obj interface{}) 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
}