blob: a675ad203e938155104a00a6808217d20293ee5b [file] [log] [blame]
Jan Schära48bd3c2024-07-29 17:22:18 +02001package object
2
3// Taken and modified from the Kubernetes plugin of CoreDNS, under Apache 2.0.
4
5import (
6 "fmt"
7 "net/netip"
8 "regexp"
9 "slices"
10 "strings"
11 "time"
12
13 api "k8s.io/api/core/v1"
14 discovery "k8s.io/api/discovery/v1"
15 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
16 "k8s.io/apimachinery/pkg/runtime"
17)
18
19// Endpoints is a stripped down discovery.EndpointSlice
20// with only the items we need.
21type Endpoints struct {
22 Version string
23 Name string
24 Namespace string
25 LastChangeTriggerTime time.Time
26 Index string
27 Addresses []EndpointAddress
28 Ports []Port
29
30 *Empty
31}
32
33// EndpointAddress is a tuple that describes single IP address.
34type EndpointAddress struct {
35 // IP contains the IP address in binary format.
36 IP string
37 Hostname string
38}
39
40// Port is a tuple that describes a single port.
41type Port struct {
42 Port uint16
43 Name string
44 Protocol string
45}
46
47// EndpointsKey returns a string using for the index.
48func EndpointsKey(name, namespace string) string { return name + "." + namespace }
49
50var hostnameRegexp = regexp.MustCompile(`^[-a-z0-9]{1,63}$`)
51
52// EndpointSliceToEndpoints converts a *discovery.EndpointSlice to a *Endpoints.
53func EndpointSliceToEndpoints(obj meta.Object) (meta.Object, error) {
54 ends, ok := obj.(*discovery.EndpointSlice)
55 if !ok {
56 return nil, fmt.Errorf("unexpected object %v", obj)
57 }
58 e := &Endpoints{
59 Version: ends.GetResourceVersion(),
60 Name: ends.GetName(),
61 Namespace: ends.GetNamespace(),
62 Index: EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()),
63 }
64
65 // In case of parse error, the value is time.Zero.
66 e.LastChangeTriggerTime, _ = time.Parse(time.RFC3339Nano, ends.Annotations[api.EndpointsLastChangeTriggerTime])
67
68 e.Ports = make([]Port, 0, len(ends.Ports))
69 for _, p := range ends.Ports {
70 if p.Port != nil && *p.Port >= 1 && *p.Port <= 0xffff &&
71 p.Name != nil && *p.Name != "" && p.Protocol != nil {
72 ep := Port{
73 Port: uint16(*p.Port),
74 Name: strings.ToLower(*p.Name),
75 Protocol: strings.ToLower(string(*p.Protocol)),
76 }
77 e.Ports = append(e.Ports, ep)
78 }
79 }
80
81 for _, end := range ends.Endpoints {
82 if !endpointsliceReady(end.Conditions.Ready) {
83 continue
84 }
85
86 var endHostname string
87 if end.Hostname != nil {
88 endHostname = *end.Hostname
89 }
90 if endHostname != "" && !hostnameRegexp.MatchString(endHostname) {
91 endHostname = ""
92 }
93
94 for _, rawIP := range end.Addresses {
95 parsedIP, err := netip.ParseAddr(rawIP)
96 if err != nil || parsedIP.Zone() != "" {
97 continue
98 }
99 parsedIP = parsedIP.Unmap()
100 // The IP address is converted to a binary string, not human readable.
101 // That way we don't need to parse it again later.
102 ea := EndpointAddress{IP: string(parsedIP.AsSlice())}
103 if endHostname != "" {
104 ea.Hostname = endHostname
105 } else {
106 ea.Hostname = strings.ReplaceAll(strings.ReplaceAll(parsedIP.String(), ".", "-"), ":", "-")
107 }
108 e.Addresses = append(e.Addresses, ea)
109 }
110 }
111
112 *ends = discovery.EndpointSlice{}
113
114 return e, nil
115}
116
117func endpointsliceReady(ready *bool) bool {
118 // Per API docs: a nil value indicates an unknown state. In most cases
119 // consumers should interpret this unknown state as ready.
120 if ready == nil {
121 return true
122 }
123 return *ready
124}
125
126var _ runtime.Object = &Endpoints{}
127
128// DeepCopyObject implements the ObjectKind interface.
129func (e *Endpoints) DeepCopyObject() runtime.Object {
130 e1 := &Endpoints{
131 Version: e.Version,
132 Name: e.Name,
133 Namespace: e.Namespace,
134 Index: e.Index,
135 Addresses: make([]EndpointAddress, len(e.Addresses)),
136 Ports: make([]Port, len(e.Ports)),
137 }
138 copy(e1.Addresses, e.Addresses)
139 copy(e1.Ports, e.Ports)
140 return e1
141}
142
143// GetNamespace implements the metav1.Object interface.
144func (e *Endpoints) GetNamespace() string { return e.Namespace }
145
146// SetNamespace implements the metav1.Object interface.
147func (e *Endpoints) SetNamespace(namespace string) {}
148
149// GetName implements the metav1.Object interface.
150func (e *Endpoints) GetName() string { return e.Name }
151
152// SetName implements the metav1.Object interface.
153func (e *Endpoints) SetName(name string) {}
154
155// GetResourceVersion implements the metav1.Object interface.
156func (e *Endpoints) GetResourceVersion() string { return e.Version }
157
158// SetResourceVersion implements the metav1.Object interface.
159func (e *Endpoints) SetResourceVersion(version string) {}
160
161// EndpointsModified checks if the update to an endpoint is something
162// that matters to us or if they are effectively equivalent.
163func EndpointsModified(a, b *Endpoints) bool {
164 if a.Index != b.Index {
165 return true
166 }
167 if !slices.Equal(a.Addresses, b.Addresses) {
168 return true
169 }
170 if !slices.Equal(a.Ports, b.Ports) {
171 return true
172 }
173 return false
174}