mirror of
https://github.com/cloudnativelabs/kube-router.git
synced 2025-09-26 18:41:05 +02:00
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.
151 lines
5.1 KiB
Go
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
|
|
}
|