blob: a675ad203e938155104a00a6808217d20293ee5b [file] [log] [blame]
package object
// Taken and modified from the Kubernetes plugin of CoreDNS, under Apache 2.0.
import (
"fmt"
"net/netip"
"regexp"
"slices"
"strings"
"time"
api "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// Endpoints is a stripped down discovery.EndpointSlice
// with only the items we need.
type Endpoints struct {
Version string
Name string
Namespace string
LastChangeTriggerTime time.Time
Index string
Addresses []EndpointAddress
Ports []Port
*Empty
}
// EndpointAddress is a tuple that describes single IP address.
type EndpointAddress struct {
// IP contains the IP address in binary format.
IP string
Hostname string
}
// Port is a tuple that describes a single port.
type Port struct {
Port uint16
Name string
Protocol string
}
// EndpointsKey returns a string using for the index.
func EndpointsKey(name, namespace string) string { return name + "." + namespace }
var hostnameRegexp = regexp.MustCompile(`^[-a-z0-9]{1,63}$`)
// EndpointSliceToEndpoints converts a *discovery.EndpointSlice to a *Endpoints.
func EndpointSliceToEndpoints(obj meta.Object) (meta.Object, error) {
ends, ok := obj.(*discovery.EndpointSlice)
if !ok {
return nil, fmt.Errorf("unexpected object %v", obj)
}
e := &Endpoints{
Version: ends.GetResourceVersion(),
Name: ends.GetName(),
Namespace: ends.GetNamespace(),
Index: EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()),
}
// In case of parse error, the value is time.Zero.
e.LastChangeTriggerTime, _ = time.Parse(time.RFC3339Nano, ends.Annotations[api.EndpointsLastChangeTriggerTime])
e.Ports = make([]Port, 0, len(ends.Ports))
for _, p := range ends.Ports {
if p.Port != nil && *p.Port >= 1 && *p.Port <= 0xffff &&
p.Name != nil && *p.Name != "" && p.Protocol != nil {
ep := Port{
Port: uint16(*p.Port),
Name: strings.ToLower(*p.Name),
Protocol: strings.ToLower(string(*p.Protocol)),
}
e.Ports = append(e.Ports, ep)
}
}
for _, end := range ends.Endpoints {
if !endpointsliceReady(end.Conditions.Ready) {
continue
}
var endHostname string
if end.Hostname != nil {
endHostname = *end.Hostname
}
if endHostname != "" && !hostnameRegexp.MatchString(endHostname) {
endHostname = ""
}
for _, rawIP := range end.Addresses {
parsedIP, err := netip.ParseAddr(rawIP)
if err != nil || parsedIP.Zone() != "" {
continue
}
parsedIP = parsedIP.Unmap()
// The IP address is converted to a binary string, not human readable.
// That way we don't need to parse it again later.
ea := EndpointAddress{IP: string(parsedIP.AsSlice())}
if endHostname != "" {
ea.Hostname = endHostname
} else {
ea.Hostname = strings.ReplaceAll(strings.ReplaceAll(parsedIP.String(), ".", "-"), ":", "-")
}
e.Addresses = append(e.Addresses, ea)
}
}
*ends = discovery.EndpointSlice{}
return e, nil
}
func endpointsliceReady(ready *bool) bool {
// Per API docs: a nil value indicates an unknown state. In most cases
// consumers should interpret this unknown state as ready.
if ready == nil {
return true
}
return *ready
}
var _ runtime.Object = &Endpoints{}
// DeepCopyObject implements the ObjectKind interface.
func (e *Endpoints) DeepCopyObject() runtime.Object {
e1 := &Endpoints{
Version: e.Version,
Name: e.Name,
Namespace: e.Namespace,
Index: e.Index,
Addresses: make([]EndpointAddress, len(e.Addresses)),
Ports: make([]Port, len(e.Ports)),
}
copy(e1.Addresses, e.Addresses)
copy(e1.Ports, e.Ports)
return e1
}
// GetNamespace implements the metav1.Object interface.
func (e *Endpoints) GetNamespace() string { return e.Namespace }
// SetNamespace implements the metav1.Object interface.
func (e *Endpoints) SetNamespace(namespace string) {}
// GetName implements the metav1.Object interface.
func (e *Endpoints) GetName() string { return e.Name }
// SetName implements the metav1.Object interface.
func (e *Endpoints) SetName(name string) {}
// GetResourceVersion implements the metav1.Object interface.
func (e *Endpoints) GetResourceVersion() string { return e.Version }
// SetResourceVersion implements the metav1.Object interface.
func (e *Endpoints) SetResourceVersion(version string) {}
// EndpointsModified checks if the update to an endpoint is something
// that matters to us or if they are effectively equivalent.
func EndpointsModified(a, b *Endpoints) bool {
if a.Index != b.Index {
return true
}
if !slices.Equal(a.Addresses, b.Addresses) {
return true
}
if !slices.Equal(a.Ports, b.Ports) {
return true
}
return false
}