Skip to content

Commit dd9ebae

Browse files
committed
feat: ipam timestamp
1 parent b38a260 commit dd9ebae

File tree

13 files changed

+91
-50
lines changed

13 files changed

+91
-50
lines changed

cmd/ipam/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ func main() {
7070

7171
// Server options.
7272
cmd.Flags().IntVar(&options.ServerOpts.Port, "port", consts.IpamPort, "The port on which to listen for incoming gRPC requests.")
73-
cmd.Flags().DurationVar(&options.ServerOpts.SyncFrequency, "sync-interval", consts.SyncInterval,
73+
cmd.Flags().DurationVar(&options.ServerOpts.SyncInterval, "sync-interval", consts.SyncInterval,
7474
"The interval at which the IPAM will synchronize the IPAM storage.")
75+
cmd.Flags().DurationVar(&options.ServerOpts.SyncGracePeriod, "sync-graceperiod", consts.SyncGracePeriod,
76+
"The grace period the sync routine wait before releasing an ip or a network.")
7577
cmd.Flags().BoolVar(&options.ServerOpts.GraphvizEnabled, "enable-graphviz", false, "Enable the graphviz output for the IPAM.")
7678
cmd.Flags().StringSliceVar(&options.ServerOpts.Pools, "pools",
7779
[]string{"10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"}, "The pools used by the IPAM.",

deployments/liqo/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
| ipam.internal.pod.priorityClassName | string | `""` | PriorityClassName (https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#pod-priority) for the IPAM pod. |
6161
| ipam.internal.pod.resources | object | `{"limits":{},"requests":{}}` | Resource requests and limits (https://kubernetes.io/docs/user-guide/compute-resources/) for the IPAM pod. |
6262
| ipam.internal.replicas | int | `1` | The number of IPAM instances to run, which can be increased for active/passive high availability. |
63+
| ipam.internal.syncGracePeriod | string | `"30s"` | |
6364
| ipam.internal.syncInterval | string | `"2m"` | Set the interval at which the IPAM pod will synchronize it's in-memory status with the local cluster. If you want to disable the synchronization, set the interval to 0. |
6465
| ipam.internalCIDR | string | `"10.80.0.0/16"` | The subnet used for the internal CIDR. These IPs are assigned to the Liqo internal-network interfaces. |
6566
| ipam.podCIDR | string | `""` | The subnet used by the pods in your cluster, in CIDR notation (e.g., 10.0.0.0/16). |

deployments/liqo/templates/liqo-ipam-deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ spec:
5252
- --pod-name=$(POD_NAME)
5353
- --port=6000
5454
- --sync-interval={{ .Values.ipam.internal.syncInterval }}
55+
- --sync-graceperiod={{ .Values.ipam.internal.syncGracePeriod }}
5556
{{- if $ha }}
5657
- --leader-election
5758
- --leader-election-namespace=$(POD_NAMESPACE)

deployments/liqo/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,8 @@ ipam:
449449
# -- Set the interval at which the IPAM pod will synchronize it's in-memory status with the local cluster.
450450
# If you want to disable the synchronization, set the interval to 0.
451451
syncInterval: 2m
452+
## -- Set the grace period the sync routine will wait before deleting an ip or a network.
453+
syncGracePeriod: 30s
452454
# -- The subnet used by the pods in your cluster, in CIDR notation (e.g., 10.0.0.0/16).
453455
podCIDR: ""
454456
# -- The subnet used by the services in you cluster, in CIDR notation (e.g., 172.16.0.0/16).

pkg/consts/ipam.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ const (
2424
IpamPort = 6000
2525
// SyncInterval is the frequency at which the IPAM should periodically sync its status.
2626
SyncInterval = 2 * time.Minute
27-
27+
// SyncGracePeriod is the time the IPAM sync routine should wait before performing a deletion.
28+
SyncGracePeriod = 30 * time.Second
2829
// NetworkNotRemappedLabelKey is the label key used to mark a Network that does not need CIDR remapping.
2930
NetworkNotRemappedLabelKey = "ipam.liqo.io/network-not-remapped"
3031
// NetworkNotRemappedLabelValue is the label value used to mark a Network that does not need CIDR remapping.

pkg/ipam/core/ipam.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ package ipamcore
1717
import (
1818
"fmt"
1919
"net/netip"
20-
"slices"
20+
"time"
2121
)
2222

2323
// Ipam represents the IPAM core structure.
@@ -72,10 +72,10 @@ func (ipam *Ipam) NetworkAcquireWithPrefix(prefix netip.Prefix) *netip.Prefix {
7272

7373
// NetworkRelease frees the network with the given prefix.
7474
// It returns the freed network or nil if the network is not found.
75-
func (ipam *Ipam) NetworkRelease(prefix netip.Prefix) *netip.Prefix {
75+
func (ipam *Ipam) NetworkRelease(prefix netip.Prefix, gracePeriod time.Duration) *netip.Prefix {
7676
for i := range ipam.roots {
7777
if isPrefixChildOf(ipam.roots[i].prefix, prefix) {
78-
if result := networkRelease(prefix, &ipam.roots[i]); result != nil {
78+
if result := networkRelease(prefix, &ipam.roots[i], gracePeriod); result != nil {
7979
return result
8080
}
8181
}
@@ -137,13 +137,13 @@ func (ipam *Ipam) IPAcquireWithAddr(prefix netip.Prefix, addr netip.Addr) (*neti
137137

138138
// IPRelease frees the IP address from the given prefix.
139139
// It returns the freed IP address or nil if the IP address is not found.
140-
func (ipam *Ipam) IPRelease(prefix netip.Prefix, addr netip.Addr) (*netip.Addr, error) {
140+
func (ipam *Ipam) IPRelease(prefix netip.Prefix, addr netip.Addr, gracePeriod time.Duration) (*netip.Addr, error) {
141141
node, err := ipam.search(prefix)
142142
if err != nil {
143143
return nil, err
144144
}
145145
if node != nil {
146-
return node.ipRelease(addr), nil
146+
return node.ipRelease(addr, gracePeriod), nil
147147
}
148148
return nil, nil
149149
}
@@ -155,7 +155,11 @@ func (ipam *Ipam) ListIPs(prefix netip.Prefix) ([]netip.Addr, error) {
155155
return nil, err
156156
}
157157
if node != nil {
158-
return slices.Clone(node.ips), nil
158+
addrs := make([]netip.Addr, len(node.ips))
159+
for i := range node.ips {
160+
addrs[i] = node.ips[i].addr
161+
}
162+
return addrs, nil
159163
}
160164
return nil, nil
161165
}

pkg/ipam/core/node.go

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,25 @@ import (
2121
"os"
2222
"path/filepath"
2323
"strings"
24+
"time"
2425
)
2526

27+
// nodeIP represents an IP address acquired by a node.
28+
type nodeIP struct {
29+
addr netip.Addr
30+
creationTimestamp time.Time
31+
}
32+
2633
// node represents a node in the binary tree.
2734
type node struct {
35+
lastUpdate time.Time
36+
2837
prefix netip.Prefix
2938
acquired bool
3039
left *node
3140
right *node
3241

33-
ips []netip.Addr
42+
ips []nodeIP
3443
lastip netip.Addr
3544
}
3645

@@ -42,7 +51,7 @@ const (
4251
)
4352

4453
func newNode(prefix netip.Prefix) node {
45-
return node{prefix: prefix}
54+
return node{prefix: prefix, lastUpdate: time.Now()}
4655
}
4756

4857
func allocateNetwork(size int, node *node) *netip.Prefix {
@@ -52,6 +61,7 @@ func allocateNetwork(size int, node *node) *netip.Prefix {
5261
if node.prefix.Bits() == size {
5362
if !node.isSplitted() {
5463
node.acquired = true
64+
node.lastUpdate = time.Now()
5565
return &node.prefix
5666
}
5767
return nil
@@ -76,6 +86,7 @@ func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix {
7686
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() {
7787
if !node.acquired && node.left == nil && node.right == nil {
7888
node.acquired = true
89+
node.lastUpdate = time.Now()
7990
return &node.prefix
8091
}
8192
return nil
@@ -95,29 +106,30 @@ func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix {
95106
return nil
96107
}
97108

98-
func networkRelease(prefix netip.Prefix, node *node) *netip.Prefix {
109+
func networkRelease(prefix netip.Prefix, node *node, gracePeriod time.Duration) *netip.Prefix {
99110
var result *netip.Prefix
100111

101112
if node == nil {
102113
return nil
103114
}
104115

105-
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() {
116+
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() && node.lastUpdate.Add(gracePeriod).Before(time.Now()) {
106117
if node.acquired {
107118
node.acquired = false
119+
node.lastUpdate = time.Now()
108120
return &node.prefix
109121
}
110122
return nil
111123
}
112124

113125
if node.left != nil && node.left.prefix.Overlaps(prefix) {
114-
result = networkRelease(prefix, node.left)
126+
result = networkRelease(prefix, node.left, gracePeriod)
115127
}
116128
if node.right != nil && node.right.prefix.Overlaps(prefix) {
117-
result = networkRelease(prefix, node.right)
129+
result = networkRelease(prefix, node.right, gracePeriod)
118130
}
119131

120-
node.merge()
132+
node.merge(gracePeriod)
121133
return result
122134
}
123135

@@ -143,7 +155,7 @@ func listNetworks(node *node) []netip.Prefix {
143155

144156
func (n *node) isAllocatedIP(ip netip.Addr) bool {
145157
for i := range n.ips {
146-
if n.ips[i].Compare(ip) == 0 {
158+
if n.ips[i].addr.Compare(ip) == 0 {
147159
return true
148160
}
149161
}
@@ -175,8 +187,9 @@ func (n *node) ipAcquire() *netip.Addr {
175187
addr = n.prefix.Addr()
176188
}
177189
if !n.isAllocatedIP(addr) {
178-
n.ips = append(n.ips, addr)
190+
n.ips = append(n.ips, nodeIP{addr: addr, creationTimestamp: time.Now()})
179191
n.lastip = addr
192+
n.lastUpdate = time.Now()
180193
return &addr
181194
}
182195
addr = addr.Next()
@@ -194,25 +207,30 @@ func (n *node) allocateIPWithAddr(addr netip.Addr) *netip.Addr {
194207
}
195208

196209
for i := range n.ips {
197-
if n.ips[i].Compare(addr) == 0 {
210+
if n.ips[i].addr.Compare(addr) == 0 {
198211
return nil
199212
}
200213
}
201214

202-
n.ips = append(n.ips, addr)
215+
n.ips = append(n.ips, nodeIP{addr: addr, creationTimestamp: time.Now()})
216+
n.lastUpdate = time.Now()
203217

204-
return &n.ips[len(n.ips)-1]
218+
return &n.ips[len(n.ips)-1].addr
205219
}
206220

207-
func (n *node) ipRelease(ip netip.Addr) *netip.Addr {
221+
func (n *node) ipRelease(ip netip.Addr, gracePeriod time.Duration) *netip.Addr {
208222
if !n.acquired {
209223
return nil
210224
}
211225

212-
for i, addr := range n.ips {
213-
if addr.Compare(ip) == 0 {
226+
for i, nodeIP := range n.ips {
227+
if !nodeIP.creationTimestamp.Add(gracePeriod).Before(time.Now()) {
228+
continue
229+
}
230+
if nodeIP.addr.Compare(ip) == 0 {
214231
n.ips = append(n.ips[:i], n.ips[i+1:]...)
215-
return &addr
232+
n.lastUpdate = time.Now()
233+
return &nodeIP.addr
216234
}
217235
}
218236
return nil
@@ -246,21 +264,30 @@ func (n *node) split() {
246264
n.insert(rightDirection, right)
247265
}
248266

249-
func (n *node) merge() {
250-
if n.left.isLeaf() && n.right.isLeaf() && !n.left.acquired && !n.right.acquired {
251-
n.left = nil
252-
n.right = nil
267+
func (n *node) merge(gracePeriod time.Duration) {
268+
if !n.left.lastUpdate.Add(gracePeriod).Before(time.Now()) || !n.right.lastUpdate.Add(gracePeriod).Before(time.Now()) {
269+
return // grace period not expired
253270
}
271+
if !n.left.isLeaf() || !n.right.isLeaf() {
272+
return
273+
}
274+
if n.left.acquired || n.right.acquired {
275+
return
276+
}
277+
278+
n.left = nil
279+
n.right = nil
280+
n.lastUpdate = time.Now()
254281
}
255282

256283
func (n *node) insert(nd nodeDirection, prefix netip.Prefix) {
257-
newNode := &node{prefix: prefix}
284+
newNode := newNode(prefix)
258285
switch nd {
259286
case leftDirection:
260-
n.left = newNode
287+
n.left = &newNode
261288
return
262289
case rightDirection:
263-
n.right = newNode
290+
n.right = &newNode
264291
return
265292
default:
266293
return
@@ -333,7 +360,7 @@ func (n *node) toGraphvizRecursive(sb *strings.Builder) {
333360
if len(n.ips) > 0 {
334361
ipsString := []string{}
335362
for i := range n.ips {
336-
ipsString = append(ipsString, n.ips[i].String())
363+
ipsString = append(ipsString, n.ips[i].addr.String())
337364
}
338365
label += "\\n" + strings.Join(ipsString, "\\n")
339366
}

pkg/ipam/initialize.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (lipam *LiqoIPAM) initializeNetworks(ctx context.Context) error {
5555
}
5656
for i := 0; i < int(netdetails.preallocated); i++ {
5757
if _, err := lipam.ipAcquire(net); err != nil {
58-
return errors.Join(err, lipam.networkRelease(net))
58+
return errors.Join(err, lipam.networkRelease(net, 0))
5959
}
6060
}
6161
}

pkg/ipam/ipam.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ type LiqoIPAM struct {
4646
type ServerOptions struct {
4747
Pools []string
4848
Port int
49-
SyncFrequency time.Duration
49+
SyncInterval time.Duration
50+
SyncGracePeriod time.Duration
5051
GraphvizEnabled bool
5152
}
5253

@@ -74,7 +75,7 @@ func New(ctx context.Context, cl client.Client, roots []string, opts *ServerOpti
7475
}
7576

7677
// Launch sync routine
77-
go lipam.sync(ctx, opts.SyncFrequency)
78+
go lipam.sync(ctx, opts.SyncInterval)
7879

7980
hs.SetServingStatus(IPAM_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
8081

@@ -118,7 +119,7 @@ func (lipam *LiqoIPAM) IPRelease(_ context.Context, req *IPReleaseRequest) (*IPR
118119
return &IPReleaseResponse{}, fmt.Errorf("failed to parse prefix %q: %w", req.GetCidr(), err)
119120
}
120121

121-
if err := lipam.ipRelease(addr, prefix); err != nil {
122+
if err := lipam.ipRelease(addr, prefix, 0); err != nil {
122123
return &IPReleaseResponse{}, err
123124
}
124125

@@ -157,7 +158,7 @@ func (lipam *LiqoIPAM) NetworkAcquire(_ context.Context, req *NetworkAcquireRequ
157158
for i := 0; i < int(req.GetPreAllocated()); i++ {
158159
_, err := lipam.ipAcquire(*remappedCidr)
159160
if err != nil {
160-
return &NetworkAcquireResponse{}, errors.Join(err, lipam.networkRelease(*remappedCidr))
161+
return &NetworkAcquireResponse{}, errors.Join(err, lipam.networkRelease(*remappedCidr, 0))
161162
}
162163
}
163164

@@ -174,7 +175,7 @@ func (lipam *LiqoIPAM) NetworkRelease(_ context.Context, req *NetworkReleaseRequ
174175
return &NetworkReleaseResponse{}, fmt.Errorf("failed to parse prefix %q: %w", req.GetCidr(), err)
175176
}
176177

177-
if err := lipam.networkRelease(prefix); err != nil {
178+
if err := lipam.networkRelease(prefix, 0); err != nil {
178179
return &NetworkReleaseResponse{}, err
179180
}
180181

pkg/ipam/ips.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"net/netip"
21+
"time"
2122

2223
klog "k8s.io/klog/v2"
2324
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -61,13 +62,13 @@ func (lipam *LiqoIPAM) ipAcquireWithAddr(addr netip.Addr, prefix netip.Prefix) e
6162
}
6263

6364
// ipRelease frees an IP, removing it from the cache.
64-
func (lipam *LiqoIPAM) ipRelease(addr netip.Addr, prefix netip.Prefix) error {
65-
result, err := lipam.IpamCore.IPRelease(prefix, addr)
65+
func (lipam *LiqoIPAM) ipRelease(addr netip.Addr, prefix netip.Prefix, gracePeriod time.Duration) error {
66+
result, err := lipam.IpamCore.IPRelease(prefix, addr, gracePeriod)
6667
if err != nil {
6768
return fmt.Errorf("error freeing IP %q (network %q): %w", addr.String(), prefix.String(), err)
6869
}
6970
if result == nil {
70-
klog.Infof("IP %q (network %q) already freed", addr.String(), prefix.String())
71+
klog.Infof("IP %q (network %q) already freed or grace period not over", addr.String(), prefix.String())
7172
return nil
7273
}
7374
klog.Infof("Freed IP %q (network %q)", addr.String(), prefix.String())

0 commit comments

Comments
 (0)