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