Skip to content

Commit 03fdfb3

Browse files
committed
feat: ipam timestamp
1 parent f7c2de7 commit 03fdfb3

File tree

15 files changed

+305
-165
lines changed

15 files changed

+305
-165
lines changed

cmd/ipam/main.go

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

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

deployments/liqo/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
| ipam.internal.pod.priorityClassName | string | `""` | PriorityClassName (https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#pod-priority) for the IPAM pod. |
6060
| ipam.internal.pod.resources | object | `{"limits":{},"requests":{}}` | Resource requests and limits (https://kubernetes.io/docs/user-guide/compute-resources/) for the IPAM pod. |
6161
| ipam.internal.replicas | int | `1` | The number of IPAM instances to run, which can be increased for active/passive high availability. |
62+
| ipam.internal.syncGracePeriod | string | `"30s"` | |
6263
| 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. |
6364
| 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. |
6465
| 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: 42 additions & 8 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.
@@ -74,10 +74,10 @@ func (ipam *Ipam) NetworkAcquireWithPrefix(prefix netip.Prefix) *netip.Prefix {
7474

7575
// NetworkRelease frees the network with the given prefix.
7676
// It returns the freed network or nil if the network is not found.
77-
func (ipam *Ipam) NetworkRelease(prefix netip.Prefix) *netip.Prefix {
77+
func (ipam *Ipam) NetworkRelease(prefix netip.Prefix, gracePeriod time.Duration) *netip.Prefix {
7878
for i := range ipam.roots {
7979
if isPrefixChildOf(ipam.roots[i].prefix, prefix) {
80-
if result := networkRelease(prefix, &ipam.roots[i]); result != nil {
80+
if result := networkRelease(prefix, &ipam.roots[i], gracePeriod); result != nil {
8181
return result
8282
}
8383
}
@@ -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,14 +155,18 @@ 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
}
162166

163-
// IsAllocatedIP checks if the IP address is allocated from the given prefix.
167+
// IPIsAlocated checks if the IP address is allocated from the given prefix.
164168
// It returns true if the IP address is allocated, false otherwise.
165-
func (ipam *Ipam) IsAllocatedIP(prefix netip.Prefix, addr netip.Addr) (bool, error) {
169+
func (ipam *Ipam) IPIsAlocated(prefix netip.Prefix, addr netip.Addr) (bool, error) {
166170
node, err := ipam.search(prefix)
167171
if err != nil {
168172
return false, err
@@ -216,3 +220,33 @@ func checkRoots(roots []netip.Prefix) error {
216220
}
217221
return nil
218222
}
223+
224+
// NetworkSetLastUpdateTimestamp sets the last update time of the network with the given prefix.
225+
// This function is for testing purposes only.
226+
func (ipam *Ipam) NetworkSetLastUpdateTimestamp(prefix netip.Prefix, lastUpdateTimestamp time.Time) error {
227+
node, err := ipam.search(prefix)
228+
if err != nil {
229+
return err
230+
}
231+
if node == nil {
232+
return fmt.Errorf("prefix %s not found", prefix)
233+
}
234+
node.lastUpdateTimestamp = lastUpdateTimestamp
235+
return nil
236+
}
237+
238+
// IPSetCreationTimestamp sets the creation timestamp of the IP address with the given address.
239+
// This function is for testing purposes only.
240+
func (ipam *Ipam) IPSetCreationTimestamp(addr netip.Addr, prefix netip.Prefix, creationTimestamp time.Time) error {
241+
node, err := ipam.search(prefix)
242+
if err != nil {
243+
return err
244+
}
245+
for i := range node.ips {
246+
if node.ips[i].addr.Compare(addr) == 0 {
247+
node.ips[i].creationTimestamp = creationTimestamp
248+
return nil
249+
}
250+
}
251+
return nil
252+
}

pkg/ipam/core/node.go

Lines changed: 60 additions & 27 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+
lastUpdateTimestamp 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

@@ -39,10 +48,12 @@ type nodeDirection string
3948
const (
4049
leftDirection nodeDirection = "left"
4150
rightDirection nodeDirection = "right"
51+
52+
graphvizFolder = "./graphviz"
4253
)
4354

4455
func newNode(prefix netip.Prefix) node {
45-
return node{prefix: prefix}
56+
return node{prefix: prefix, lastUpdateTimestamp: time.Now()}
4657
}
4758

4859
func allocateNetwork(size int, node *node) *netip.Prefix {
@@ -52,6 +63,7 @@ func allocateNetwork(size int, node *node) *netip.Prefix {
5263
if node.prefix.Bits() == size {
5364
if !node.isSplitted() {
5465
node.acquired = true
66+
node.lastUpdateTimestamp = time.Now()
5567
return &node.prefix
5668
}
5769
return nil
@@ -76,6 +88,7 @@ func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix {
7688
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() {
7789
if !node.acquired && node.left == nil && node.right == nil {
7890
node.acquired = true
91+
node.lastUpdateTimestamp = time.Now()
7992
return &node.prefix
8093
}
8194
return nil
@@ -95,29 +108,31 @@ func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix {
95108
return nil
96109
}
97110

98-
func networkRelease(prefix netip.Prefix, node *node) *netip.Prefix {
111+
func networkRelease(prefix netip.Prefix, node *node, gracePeriod time.Duration) *netip.Prefix {
99112
var result *netip.Prefix
100113

101114
if node == nil {
102115
return nil
103116
}
104117

105-
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() {
118+
if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() &&
119+
node.lastUpdateTimestamp.Add(gracePeriod).Before(time.Now()) {
106120
if node.acquired {
107121
node.acquired = false
122+
node.lastUpdateTimestamp = time.Now()
108123
return &node.prefix
109124
}
110125
return nil
111126
}
112127

113128
if node.left != nil && node.left.prefix.Overlaps(prefix) {
114-
result = networkRelease(prefix, node.left)
129+
result = networkRelease(prefix, node.left, gracePeriod)
115130
}
116131
if node.right != nil && node.right.prefix.Overlaps(prefix) {
117-
result = networkRelease(prefix, node.right)
132+
result = networkRelease(prefix, node.right, gracePeriod)
118133
}
119134

120-
node.merge()
135+
node.merge(gracePeriod)
121136
return result
122137
}
123138

@@ -170,7 +185,7 @@ func listNetworks(node *node) []netip.Prefix {
170185

171186
func (n *node) isAllocatedIP(ip netip.Addr) bool {
172187
for i := range n.ips {
173-
if n.ips[i].Compare(ip) == 0 {
188+
if n.ips[i].addr.Compare(ip) == 0 {
174189
return true
175190
}
176191
}
@@ -202,8 +217,9 @@ func (n *node) ipAcquire() *netip.Addr {
202217
addr = n.prefix.Addr()
203218
}
204219
if !n.isAllocatedIP(addr) {
205-
n.ips = append(n.ips, addr)
220+
n.ips = append(n.ips, nodeIP{addr: addr, creationTimestamp: time.Now()})
206221
n.lastip = addr
222+
n.lastUpdateTimestamp = time.Now()
207223
return &addr
208224
}
209225
addr = addr.Next()
@@ -221,25 +237,30 @@ func (n *node) allocateIPWithAddr(addr netip.Addr) *netip.Addr {
221237
}
222238

223239
for i := range n.ips {
224-
if n.ips[i].Compare(addr) == 0 {
240+
if n.ips[i].addr.Compare(addr) == 0 {
225241
return nil
226242
}
227243
}
228244

229-
n.ips = append(n.ips, addr)
245+
n.ips = append(n.ips, nodeIP{addr: addr, creationTimestamp: time.Now()})
246+
n.lastUpdateTimestamp = time.Now()
230247

231-
return &n.ips[len(n.ips)-1]
248+
return &n.ips[len(n.ips)-1].addr
232249
}
233250

234-
func (n *node) ipRelease(ip netip.Addr) *netip.Addr {
251+
func (n *node) ipRelease(ip netip.Addr, gracePeriod time.Duration) *netip.Addr {
235252
if !n.acquired {
236253
return nil
237254
}
238255

239-
for i, addr := range n.ips {
240-
if addr.Compare(ip) == 0 {
256+
for i, nodeIP := range n.ips {
257+
if !nodeIP.creationTimestamp.Add(gracePeriod).Before(time.Now()) {
258+
continue
259+
}
260+
if nodeIP.addr.Compare(ip) == 0 {
241261
n.ips = append(n.ips[:i], n.ips[i+1:]...)
242-
return &addr
262+
n.lastUpdateTimestamp = time.Now()
263+
return &nodeIP.addr
243264
}
244265
}
245266
return nil
@@ -273,21 +294,33 @@ func (n *node) split() {
273294
n.insert(rightDirection, right)
274295
}
275296

276-
func (n *node) merge() {
277-
if n.left.isLeaf() && n.right.isLeaf() && !n.left.acquired && !n.right.acquired {
278-
n.left = nil
279-
n.right = nil
297+
func (n *node) merge(gracePeriod time.Duration) {
298+
if n.left == nil || n.right == nil {
299+
return
300+
}
301+
if !n.left.lastUpdateTimestamp.Add(gracePeriod).Before(time.Now()) || !n.right.lastUpdateTimestamp.Add(gracePeriod).Before(time.Now()) {
302+
return // grace period not expired
303+
}
304+
if !n.left.isLeaf() || !n.right.isLeaf() {
305+
return
280306
}
307+
if n.left.acquired || n.right.acquired {
308+
return
309+
}
310+
311+
n.left = nil
312+
n.right = nil
313+
n.lastUpdateTimestamp = time.Now()
281314
}
282315

283316
func (n *node) insert(nd nodeDirection, prefix netip.Prefix) {
284-
newNode := &node{prefix: prefix}
317+
newNode := newNode(prefix)
285318
switch nd {
286319
case leftDirection:
287-
n.left = newNode
320+
n.left = &newNode
288321
return
289322
case rightDirection:
290-
n.right = newNode
323+
n.right = &newNode
291324
return
292325
default:
293326
return
@@ -335,13 +368,13 @@ func (n *node) toGraphviz() error {
335368
n.toGraphvizRecursive(&sb)
336369
sb.WriteString("}\n")
337370

338-
if _, err := os.Stat("./graphviz"); os.IsNotExist(err) {
339-
if err := os.Mkdir("./graphviz", 0o700); err != nil {
371+
if _, err := os.Stat(graphvizFolder + ""); os.IsNotExist(err) {
372+
if err := os.Mkdir(graphvizFolder+"", 0o700); err != nil {
340373
return err
341374
}
342375
}
343376

344-
filePath := filepath.Clean("./graphviz/" + strings.NewReplacer("/", "_", ".", "_").Replace(n.prefix.String()) + ".dot")
377+
filePath := filepath.Clean(graphvizFolder + "/" + strings.NewReplacer("/", "_", ".", "_").Replace(n.prefix.String()) + ".dot")
345378
file, err := os.Create(filePath)
346379
if err != nil {
347380
return err
@@ -360,7 +393,7 @@ func (n *node) toGraphvizRecursive(sb *strings.Builder) {
360393
if len(n.ips) > 0 {
361394
ipsString := []string{}
362395
for i := range n.ips {
363-
ipsString = append(ipsString, n.ips[i].String())
396+
ipsString = append(ipsString, n.ips[i].addr.String())
364397
}
365398
label += "\\n" + strings.Join(ipsString, "\\n")
366399
}

pkg/ipam/graphviz/10_0_0_0_8.dot

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
digraph G {
2+
"10.0.0.0/8" -> "10.0.0.0/9";
3+
"10.0.0.0/9" -> "10.0.0.0/10";
4+
"10.0.0.0/10" -> "10.0.0.0/11";
5+
"10.0.0.0/11" -> "10.0.0.0/12";
6+
"10.0.0.0/12" -> "10.0.0.0/13";
7+
"10.0.0.0/13" -> "10.0.0.0/14";
8+
"10.0.0.0/14" -> "10.0.0.0/15";
9+
"10.0.0.0/15" -> "10.0.0.0/16";
10+
"10.0.0.0/16" [label="10.0.0.0/16", style=filled, color="#57cc99"];
11+
"10.0.0.0/15" -> "10.1.0.0/16";
12+
"10.1.0.0/16" [label="10.1.0.0/16", style=filled, color="#57cc99"];
13+
"10.0.0.0/14" -> "10.2.0.0/15";
14+
"10.2.0.0/15" -> "10.2.0.0/16";
15+
"10.2.0.0/16" [label="10.2.0.0/16", style=filled, color="#57cc99"];
16+
"10.2.0.0/15" -> "10.3.0.0/16";
17+
"10.0.0.0/13" -> "10.4.0.0/14";
18+
"10.4.0.0/14" -> "10.4.0.0/15";
19+
"10.4.0.0/15" -> "10.4.0.0/16";
20+
"10.4.0.0/15" -> "10.5.0.0/16";
21+
"10.4.0.0/14" -> "10.6.0.0/15";
22+
"10.0.0.0/12" -> "10.8.0.0/13";
23+
"10.0.0.0/11" -> "10.16.0.0/12";
24+
"10.0.0.0/10" -> "10.32.0.0/11";
25+
"10.0.0.0/9" -> "10.64.0.0/10";
26+
"10.0.0.0/8" -> "10.128.0.0/9";
27+
}

pkg/ipam/initialize.go

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

0 commit comments

Comments
 (0)