diff --git a/10_0_0_0_8.dot b/10_0_0_0_8.dot new file mode 100644 index 0000000000..db68015852 --- /dev/null +++ b/10_0_0_0_8.dot @@ -0,0 +1,48 @@ +digraph G { + "10.0.0.0/8" -> "10.0.0.0/9"; + "10.0.0.0/9" -> "10.0.0.0/10"; + "10.0.0.0/9" -> "10.64.0.0/10"; + "10.64.0.0/10" -> "10.64.0.0/11"; + "10.64.0.0/11" -> "10.64.0.0/12"; + "10.64.0.0/12" -> "10.64.0.0/13"; + "10.64.0.0/13" -> "10.64.0.0/14"; + "10.64.0.0/13" -> "10.68.0.0/14"; + "10.68.0.0/14" -> "10.68.0.0/15"; + "10.68.0.0/14" -> "10.70.0.0/15"; + "10.70.0.0/15" -> "10.70.0.0/16"; + "10.70.0.0/16" [label="10.70.0.0/16\n10.70.0.0\n10.70.0.1", style=filled, color="#57cc99"]; + "10.70.0.0/15" -> "10.71.0.0/16"; + "10.64.0.0/12" -> "10.72.0.0/13"; + "10.64.0.0/11" -> "10.80.0.0/12"; + "10.80.0.0/12" -> "10.80.0.0/13"; + "10.80.0.0/13" -> "10.80.0.0/14"; + "10.80.0.0/14" -> "10.80.0.0/15"; + "10.80.0.0/15" -> "10.80.0.0/16"; + "10.80.0.0/16" [label="10.80.0.0/16", style=filled, color="#57cc99"]; + "10.80.0.0/15" -> "10.81.0.0/16"; + "10.80.0.0/14" -> "10.82.0.0/15"; + "10.80.0.0/13" -> "10.84.0.0/14"; + "10.80.0.0/12" -> "10.88.0.0/13"; + "10.64.0.0/10" -> "10.96.0.0/11"; + "10.96.0.0/11" -> "10.96.0.0/12"; + "10.96.0.0/12" -> "10.96.0.0/13"; + "10.96.0.0/12" -> "10.104.0.0/13"; + "10.104.0.0/13" -> "10.104.0.0/14"; + "10.104.0.0/13" -> "10.108.0.0/14"; + "10.108.0.0/14" -> "10.108.0.0/15"; + "10.108.0.0/14" -> "10.110.0.0/15"; + "10.110.0.0/15" -> "10.110.0.0/16"; + "10.110.0.0/15" -> "10.111.0.0/16"; + "10.111.0.0/16" [label="10.111.0.0/16", style=filled, color="#57cc99"]; + "10.96.0.0/11" -> "10.112.0.0/12"; + "10.112.0.0/12" -> "10.112.0.0/13"; + "10.112.0.0/13" -> "10.112.0.0/14"; + "10.112.0.0/14" -> "10.112.0.0/15"; + "10.112.0.0/15" -> "10.112.0.0/16"; + "10.112.0.0/16" [label="10.112.0.0/16", style=filled, color="#57cc99"]; + "10.112.0.0/15" -> "10.113.0.0/16"; + "10.112.0.0/14" -> "10.114.0.0/15"; + "10.112.0.0/13" -> "10.116.0.0/14"; + "10.112.0.0/12" -> "10.120.0.0/13"; + "10.0.0.0/8" -> "10.128.0.0/9"; +} diff --git a/192_168_0_0_16.dot b/192_168_0_0_16.dot new file mode 100644 index 0000000000..2f3e9ec78e --- /dev/null +++ b/192_168_0_0_16.dot @@ -0,0 +1,2 @@ +digraph G { +} diff --git a/build/common/Dockerfile b/build/common/Dockerfile index 2ce13aa233..04bb699e45 100644 --- a/build/common/Dockerfile +++ b/build/common/Dockerfile @@ -17,7 +17,10 @@ FROM alpine:3.20.3 RUN apk update && \ apk add --no-cache ca-certificates && \ update-ca-certificates && \ - rm -rf /var/cache/apk/* + rm -rf /var/cache/apk/* && \ + mkdir -p /app && \ + chown 1000:1000 /app && \ + chmod 700 /app ARG COMPONENT COPY --from=builder /tmp/builder/$COMPONENT /usr/bin/$COMPONENT diff --git a/cmd/ipam/main.go b/cmd/ipam/main.go index 79c926a05d..e5cbfa391a 100644 --- a/cmd/ipam/main.go +++ b/cmd/ipam/main.go @@ -132,7 +132,9 @@ func run(cmd *cobra.Command, _ []string) error { } } - liqoIPAM, err := ipam.New(ctx, cl, &options.ServerOpts) + liqoIPAM, err := ipam.New(ctx, cl, []string{ + "10.0.0.0/8", "192.168.0.0/16", + }, &options.ServerOpts) if err != nil { return err } diff --git a/deployments/liqo/templates/liqo-ipam-deployment.yaml b/deployments/liqo/templates/liqo-ipam-deployment.yaml index 7ced18abe2..a3a7c0a6fd 100644 --- a/deployments/liqo/templates/liqo-ipam-deployment.yaml +++ b/deployments/liqo/templates/liqo-ipam-deployment.yaml @@ -26,6 +26,8 @@ spec: {{- toYaml .Values.ipam.internal.pod.labels | nindent 8 }} {{- end }} spec: + strategy: + type: Recreate securityContext: {{- include "liqo.podSecurityContext" . | nindent 8 }} serviceAccountName: {{ include "liqo.prefixedName" $ipamConfig }} diff --git a/graphviz/10_0_0_0_8.dot b/graphviz/10_0_0_0_8.dot new file mode 100644 index 0000000000..23292211c0 --- /dev/null +++ b/graphviz/10_0_0_0_8.dot @@ -0,0 +1,63 @@ +digraph G { + "10.0.0.0/8" -> "10.0.0.0/9"; + "10.0.0.0/9" -> "10.0.0.0/10"; + "10.0.0.0/9" -> "10.64.0.0/10"; + "10.64.0.0/10" -> "10.64.0.0/11"; + "10.64.0.0/11" -> "10.64.0.0/12"; + "10.64.0.0/12" -> "10.64.0.0/13"; + "10.64.0.0/13" -> "10.64.0.0/14"; + "10.64.0.0/13" -> "10.68.0.0/14"; + "10.68.0.0/14" -> "10.68.0.0/15"; + "10.68.0.0/14" -> "10.70.0.0/15"; + "10.70.0.0/15" -> "10.70.0.0/16"; + "10.70.0.0/16" [label="10.70.0.0/16\n10.70.0.1\n10.70.0.0\n10.70.0.2", style=filled, color="#57cc99"]; + "10.70.0.0/15" -> "10.71.0.0/16"; + "10.64.0.0/12" -> "10.72.0.0/13"; + "10.64.0.0/11" -> "10.80.0.0/12"; + "10.80.0.0/12" -> "10.80.0.0/13"; + "10.80.0.0/13" -> "10.80.0.0/14"; + "10.80.0.0/14" -> "10.80.0.0/15"; + "10.80.0.0/15" -> "10.80.0.0/16"; + "10.80.0.0/16" [label="10.80.0.0/16", style=filled, color="#57cc99"]; + "10.80.0.0/15" -> "10.81.0.0/16"; + "10.80.0.0/14" -> "10.82.0.0/15"; + "10.80.0.0/13" -> "10.84.0.0/14"; + "10.80.0.0/12" -> "10.88.0.0/13"; + "10.64.0.0/10" -> "10.96.0.0/11"; + "10.96.0.0/11" -> "10.96.0.0/12"; + "10.96.0.0/12" -> "10.96.0.0/13"; + "10.96.0.0/12" -> "10.104.0.0/13"; + "10.104.0.0/13" -> "10.104.0.0/14"; + "10.104.0.0/13" -> "10.108.0.0/14"; + "10.108.0.0/14" -> "10.108.0.0/15"; + "10.108.0.0/14" -> "10.110.0.0/15"; + "10.110.0.0/15" -> "10.110.0.0/16"; + "10.110.0.0/15" -> "10.111.0.0/16"; + "10.111.0.0/16" [label="10.111.0.0/16", style=filled, color="#57cc99"]; + "10.96.0.0/11" -> "10.112.0.0/12"; + "10.112.0.0/12" -> "10.112.0.0/13"; + "10.112.0.0/13" -> "10.112.0.0/14"; + "10.112.0.0/14" -> "10.112.0.0/15"; + "10.112.0.0/15" -> "10.112.0.0/16"; + "10.112.0.0/16" [label="10.112.0.0/16", style=filled, color="#57cc99"]; + "10.112.0.0/15" -> "10.113.0.0/16"; + "10.112.0.0/14" -> "10.114.0.0/15"; + "10.112.0.0/13" -> "10.116.0.0/14"; + "10.112.0.0/12" -> "10.120.0.0/13"; + "10.0.0.0/8" -> "10.128.0.0/9"; + "10.128.0.0/9" -> "10.128.0.0/10"; + "10.128.0.0/10" -> "10.128.0.0/11"; + "10.128.0.0/11" -> "10.128.0.0/12"; + "10.128.0.0/12" -> "10.128.0.0/13"; + "10.128.0.0/13" -> "10.128.0.0/14"; + "10.128.0.0/13" -> "10.132.0.0/14"; + "10.132.0.0/14" -> "10.132.0.0/15"; + "10.132.0.0/15" -> "10.132.0.0/16"; + "10.132.0.0/16" [label="10.132.0.0/16", style=filled, color="#57cc99"]; + "10.132.0.0/15" -> "10.133.0.0/16"; + "10.132.0.0/14" -> "10.134.0.0/15"; + "10.128.0.0/12" -> "10.136.0.0/13"; + "10.128.0.0/11" -> "10.144.0.0/12"; + "10.128.0.0/10" -> "10.160.0.0/11"; + "10.128.0.0/9" -> "10.192.0.0/10"; +} diff --git a/graphviz/192_168_0_0_16.dot b/graphviz/192_168_0_0_16.dot new file mode 100644 index 0000000000..2f3e9ec78e --- /dev/null +++ b/graphviz/192_168_0_0_16.dot @@ -0,0 +1,2 @@ +digraph G { +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000..8ed866d657 --- /dev/null +++ b/main.go @@ -0,0 +1,150 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "net" + "os" + "time" + + "github.com/spf13/pflag" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health/grpc_health_v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/liqotech/liqo/pkg/consts" + "github.com/liqotech/liqo/pkg/ipam" + "github.com/liqotech/liqo/pkg/utils/restcfg" +) + +func flags(options *ipam.Options) { + pflag.IntVar(&options.ServerOpts.Port, "port", consts.IpamPort, "The port on which to listen for incoming gRPC requests.") + pflag.DurationVar(&options.ServerOpts.SyncFrequency, "interval", consts.SyncFrequency, + "The interval at which the IPAM will synchronize the IPAM storage.") + + // Leader election flags. + pflag.BoolVar(&options.EnableLeaderElection, "leader-election", false, "Enable leader election for IPAM. "+ + "Enabling this will ensure there is only one active IPAM.") + pflag.StringVar(&options.LeaderElectionNamespace, "leader-election-namespace", consts.DefaultLiqoNamespace, + "The namespace in which the leader election lease will be created.") + pflag.StringVar(&options.LeaderElectionName, "leader-election-name", "liqo-ipam-leaderelection", + "The name of the leader election lease.") + pflag.DurationVar(&options.LeaseDuration, "lease-duration", 15*time.Second, + "The duration that non-leader candidates will wait to force acquire leadership.") + pflag.DurationVar(&options.RenewDeadline, "renew-deadline", 10*time.Second, + "The duration that the acting IPAM will retry refreshing leadership before giving up.") + pflag.DurationVar(&options.RetryPeriod, "retry-period", 5*time.Second, + "The duration the LeaderElector clients should wait between tries of actions.") + pflag.StringVar(&options.PodName, "pod-name", "", + "The name of the pod running the IPAM service.") + pflag.Parse() +} + +func startServer(ctx context.Context, options *ipam.Options) { + scheme := runtime.NewScheme() + + // Set controller-runtime logger. + log.SetLogger(klog.NewKlogr()) + + // Get the rest config. + cfg := restcfg.SetRateLimiter(ctrl.GetConfigOrDie()) + + // Get the client. + cl, err := client.New(cfg, client.Options{ + Scheme: scheme, + }) + if err != nil { + klog.Error(err) + os.Exit(1) + } + + liqoIPAM, err := ipam.New(ctx, cl, []string{ + "10.0.0.0/8", "192.168.0.0/16", + }, &options.ServerOpts) + if err != nil { + klog.Error(err) + os.Exit(1) + } + + lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", options.ServerOpts.Port)) + if err != nil { + klog.Error(err) + os.Exit(1) + } + + server := grpc.NewServer() + + // Register health service + grpc_health_v1.RegisterHealthServer(server, liqoIPAM.HealthServer) + + // Register IPAM service + ipam.RegisterIPAMServer(server, liqoIPAM) + + klog.Infof("starting the IPAM server") + go func() { + if err := server.Serve(lis); err != nil { + klog.Errorf("failed to serve: %v", err) + os.Exit(1) + } + }() +} + +func main() { + ctx := context.Background() + var options ipam.Options + + flags(&options) + + startServer(ctx, &options) + + time.Sleep(2 * time.Second) + + klog.Infof("connecting to the IPAM server") + conn, err := grpc.NewClient("localhost:6000", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + klog.Errorf("failed to establish a connection to the IPAM") + os.Exit(1) + } + defer conn.Close() + cl := ipam.NewIPAMClient(conn) + + resultnet, err := cl.NetworkAcquire(ctx, &ipam.NetworkAcquireRequest{ + Immutable: true, + Cidr: "10.0.0.0/24", + }) + if err != nil { + klog.Errorf("failed to acquire the network: %v", err) + os.Exit(1) + } + klog.Infof("acquired network: %v", resultnet.Cidr) + + resultip, err := cl.IPAcquire(ctx, &ipam.IPAcquireRequest{ + Cidr: "10.0.0.0/24", + }) + + if err != nil { + klog.Errorf("failed to acquire the IP: %v", err) + os.Exit(1) + } + + klog.Infof("acquired IP: %v", resultip.Ip) +} diff --git a/pkg/ipam/core/doc.go b/pkg/ipam/core/doc.go new file mode 100644 index 0000000000..bc16e2b48a --- /dev/null +++ b/pkg/ipam/core/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ipamcore provides the core functionality for the IPAM service. +package ipamcore diff --git a/pkg/ipam/core/ipam.go b/pkg/ipam/core/ipam.go new file mode 100644 index 0000000000..1f7e4419d7 --- /dev/null +++ b/pkg/ipam/core/ipam.go @@ -0,0 +1,216 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipamcore + +import ( + "fmt" + "net/netip" + + "k8s.io/klog/v2" +) + +// Ipam represents the IPAM core structure. +type Ipam struct { + roots []node +} + +// NewIpam creates a new IPAM instance. +func NewIpam(roots, preallocated []string) (*Ipam, error) { + ipamRootsPrefixes := make([]netip.Prefix, len(roots)) + for i, root := range roots { + ipamRootsPrefixes[i] = netip.MustParsePrefix(root) + } + + ipamPreallocated := make([]netip.Prefix, len(preallocated)) + for i, prefix := range preallocated { + ipamPreallocated[i] = netip.MustParsePrefix(prefix) + } + + if err := checkRoots(ipamRootsPrefixes); err != nil { + return nil, err + } + + if err := checkPreallocated(ipamRootsPrefixes, ipamPreallocated); err != nil { + return nil, err + } + + ipamRoots := make([]node, len(roots)) + for i := range ipamRootsPrefixes { + ipamRoots[i] = newNode(ipamRootsPrefixes[i]) + } + + ipam := &Ipam{ + roots: ipamRoots, + } + + if err := ipam.preallocateNetwork(ipamRootsPrefixes, ipamPreallocated); err != nil { + return nil, err + } + + return ipam, nil +} + +// AllocateNetwork allocates a network of the given size. +// It returns the allocated network or nil if no network is available. +func (ipam *Ipam) AllocateNetwork(size int) *netip.Prefix { + for i := range ipam.roots { + if result := allocateNetwork(size, &ipam.roots[i]); result != nil { + return result + } + } + return nil +} + +// AllocateNetworkWithPrefix allocates a network with the given prefix. +// It returns the allocated network or nil if the network is not available. +func (ipam *Ipam) AllocateNetworkWithPrefix(prefix netip.Prefix) *netip.Prefix { + for i := range ipam.roots { + if result := allocateNetworkWithPrefix(prefix, &ipam.roots[i]); result != nil { + return result + } + } + return nil +} + +// FreeNetwork frees the network with the given prefix. +// It returns the freed network or nil if the network is not found. +func (ipam *Ipam) FreeNetwork(prefix netip.Prefix) *netip.Prefix { + for i := range ipam.roots { + if isPrefixChildOf(ipam.roots[i].prefix, prefix) { + if result := freeNetwork(prefix, &ipam.roots[i]); result != nil { + return result + } + } + } + return nil +} + +// ListNetworks returns the list of allocated networks. +func (ipam *Ipam) ListNetworks() []netip.Prefix { + var networks []netip.Prefix + for i := range ipam.roots { + networks = append(networks, listNetworks(&ipam.roots[i])...) + } + return networks +} + +// IsAllocatedNetwork checks if the network with the given prefix is allocated. +// It returns true if the network is allocated, false otherwise. +func (ipam *Ipam) IsAllocatedNetwork(prefix netip.Prefix) bool { + if node := ipam.search(prefix); node != nil { + return node.acquired + } + return false +} + +// AllocateIP allocates an IP address from the given prefix. +// It returns the allocated IP address or nil if the IP address is not available. +func (ipam *Ipam) AllocateIP(prefix netip.Prefix) *netip.Addr { + if node := ipam.search(prefix); node != nil { + return node.allocateIP() + } + return nil +} + +// AllocateIPWithAddr allocates the IP address from the given prefix. +// It returns the allocated IP address or nil if the IP address is not available. +func (ipam *Ipam) AllocateIPWithAddr(prefix netip.Prefix, addr netip.Addr) *netip.Addr { + if !prefix.Contains(addr) { + return nil + } + if node := ipam.search(prefix); node != nil { + return node.allocateIPWithAddr(addr) + } + return nil +} + +// FreeIP frees the IP address from the given prefix. +// It returns the freed IP address or nil if the IP address is not found. +func (ipam *Ipam) FreeIP(prefix netip.Prefix, addr netip.Addr) *netip.Addr { + if node := ipam.search(prefix); node != nil { + return node.freeIP(addr) + } + return nil +} + +// ListIPs returns the list of allocated IP addresses from the given prefix. +func (ipam *Ipam) ListIPs(prefix netip.Prefix) []netip.Addr { + if node := ipam.search(prefix); node != nil { + return node.listIPs() + } + return nil +} + +// ToGraphviz generates the Graphviz representation of the IPAM structure. +func (ipam *Ipam) ToGraphviz() { + for i := range ipam.roots { + _ = i + if err := ipam.roots[i].toGraphviz("/app"); err != nil { + klog.Warning(fmt.Sprintf("error generating Graphviz representation: %v", err)) + } + } +} + +func (ipam *Ipam) search(prefix netip.Prefix) *node { + for i := range ipam.roots { + if node := search(prefix, &ipam.roots[i]); node != nil { + return node + } + } + return nil +} + +func checkRoots(roots []netip.Prefix) error { + for i := range roots { + if err := checkHostBitsZero(roots[i]); err != nil { + return err + } + } + return nil +} + +func checkPreallocated(roots, preallocated []netip.Prefix) error { + var err error + for i := range preallocated { + if err = checkHostBitsZero(preallocated[i]); err != nil { + return err + } + isChild := false + for j := range roots { + if isPrefixChildOf(roots[j], preallocated[i]) { + isChild = true + break + } + isChild = false + } + if !isChild { + return fmt.Errorf("prefix %s is not a child of any root cidr", preallocated[i]) + } + } + return nil +} + +func (ipam *Ipam) preallocateNetwork(roots, prefixes []netip.Prefix) error { + for i := range prefixes { + for j := range roots { + if isPrefixChildOf(roots[j], prefixes[i]) { + if prefix := ipam.AllocateNetworkWithPrefix(prefixes[i]); prefix == nil { + return fmt.Errorf("prefix %s is not allocated", prefixes[i]) + } + } + } + } + return nil +} diff --git a/pkg/ipam/core/net.go b/pkg/ipam/core/net.go new file mode 100644 index 0000000000..2dd8fd9fd7 --- /dev/null +++ b/pkg/ipam/core/net.go @@ -0,0 +1,81 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipamcore + +import ( + "fmt" + "net/netip" + "strings" +) + +// convertByteSliceToString converts a slice of bytes to a comma-separated string. +func convertByteSliceToString(byteSlice []byte) string { + strSlice := make([]string, len(byteSlice)) + for i, b := range byteSlice { + strSlice[i] = fmt.Sprintf("%d", b) + } + return strings.Join(strSlice, ".") +} + +// setBit sets the bit at the given position to 1. +func setBit(b, position byte) byte { + if position > 7 { + fmt.Println("Bit position out of range") + return b + } + return b | (1 << (7 - position)) +} + +func checkHostBitsZero(prefix netip.Prefix) error { + if prefix.Masked().Addr().Compare(prefix.Addr()) != 0 { + return fmt.Errorf("%s :host bits must be zero", prefix) + } + return nil +} + +func splitNetworkPrefix(prefix netip.Prefix) (left, right netip.Prefix) { + if err := checkHostBitsZero(prefix); err != nil { + panic("Host bits must be zero") + } + + bin, err := prefix.MarshalBinary() + if err != nil { + panic(err) + } + + maskLen := bin[len(bin)-1] + + left = netip.MustParsePrefix( + fmt.Sprintf("%s/%d", convertByteSliceToString(bin[:4]), maskLen+1), + ) + + byteIndex := maskLen / 8 + bitIndex := maskLen % 8 + + bin[byteIndex] = setBit(bin[byteIndex], bitIndex) + + right = netip.MustParsePrefix( + fmt.Sprintf("%s/%d", convertByteSliceToString(bin[:4]), maskLen+1), + ) + + return left, right +} + +func isPrefixChildOf(parent, child netip.Prefix) bool { + if parent.Bits() <= child.Bits() && parent.Overlaps(child) { + return true + } + return false +} diff --git a/pkg/ipam/core/node.go b/pkg/ipam/core/node.go new file mode 100644 index 0000000000..d2d9ac1f8c --- /dev/null +++ b/pkg/ipam/core/node.go @@ -0,0 +1,350 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipamcore + +import ( + "fmt" + "math" + "net/netip" + "os" + "strings" +) + +// node represents a node in the binary tree. +type node struct { + prefix netip.Prefix + acquired bool + left *node + right *node + + ips []netip.Addr + lastIPindex int +} + +type nodeDirection string + +const ( + leftDirection nodeDirection = "left" + rightDirection nodeDirection = "right" +) + +func newNode(prefix netip.Prefix) node { + return node{prefix: prefix} +} + +func allocateNetwork(size int, node *node) *netip.Prefix { + if node.acquired || node.prefix.Bits() > size { + return nil + } + if node.prefix.Bits() == size { + if !node.isSplit() { + node.acquired = true + return &node.prefix + } + return nil + } + + if !node.isSplit() { + node.split() + } + + first, second := node.bestDirection() + + if prefix := allocateNetwork(size, node.next(first)); prefix != nil { + return prefix + } + return allocateNetwork(size, node.next(second)) +} + +func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix { + if node.acquired || !node.prefix.Overlaps(prefix) { + return nil + } + if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { + if !node.acquired && node.left == nil && node.right == nil { + node.acquired = true + return &node.prefix + } + return nil + } + + if !node.isSplit() { + node.split() + } + + if node.left != nil && node.left.prefix.Overlaps(prefix) { + return allocateNetworkWithPrefix(prefix, node.left) + } + if node.right != nil && node.right.prefix.Overlaps(prefix) { + return allocateNetworkWithPrefix(prefix, node.right) + } + + return nil +} + +func freeNetwork(prefix netip.Prefix, node *node) *netip.Prefix { + var result *netip.Prefix + + if node == nil { + return nil + } + + if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { + if node.acquired { + node.acquired = false + return &node.prefix + } + return nil + } + + if node.left != nil && node.left.prefix.Overlaps(prefix) { + result = freeNetwork(prefix, node.left) + } + if node.right != nil && node.right.prefix.Overlaps(prefix) { + result = freeNetwork(prefix, node.right) + } + + node.merge() + return result +} + +func listNetworks(node *node) []netip.Prefix { + if node == nil { + return nil + } + + if node.acquired { + return []netip.Prefix{node.prefix} + } + + var networks []netip.Prefix + if node.left != nil { + networks = append(networks, listNetworks(node.left)...) + } + if node.right != nil { + networks = append(networks, listNetworks(node.right)...) + } + + return networks +} + +func (n *node) isAllocatedIP(ip netip.Addr) bool { + for i := range n.ips { + if n.ips[i].Compare(ip) == 0 { + return true + } + } + return false +} + +func (n *node) allocateIP() *netip.Addr { + if !n.acquired { + return nil + } + + size := int(math.Pow(2, float64(n.prefix.Addr().BitLen()-n.prefix.Bits()))) + + addr := n.prefix.Addr() + if len(n.ips) != 0 { + addr = n.ips[n.lastIPindex] + } + + for i := 0; i < size; i++ { + if !n.isAllocatedIP(addr) { + n.ips = append(n.ips, addr) + n.lastIPindex = len(n.ips) - 1 + return &addr + } + addr = addr.Next() + if !n.prefix.Contains(addr) { + addr = n.prefix.Addr() + } + } + return nil +} + +func (n *node) allocateIPWithAddr(addr netip.Addr) *netip.Addr { + if !n.acquired { + return nil + } + + if !n.prefix.Contains(addr) { + return nil + } + + for i := range n.ips { + if n.ips[i].Compare(addr) == 0 { + return nil + } + } + + n.ips = append(n.ips, addr) + + return &n.ips[len(n.ips)-1] +} + +func (n *node) freeIP(ip netip.Addr) *netip.Addr { + if !n.acquired { + return nil + } + + for i, addr := range n.ips { + if addr.Compare(ip) == 0 { + if i <= n.lastIPindex { + n.lastIPindex-- + } + n.ips = append(n.ips[:i], n.ips[i+1:]...) + return &addr + } + } + return nil +} + +func (n *node) listIPs() []netip.Addr { + return n.ips +} + +func search(prefix netip.Prefix, node *node) *node { + if node == nil { + return nil + } + + if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { + return node + } + + if node.left != nil && node.left.prefix.Overlaps(prefix) { + return search(prefix, node.left) + } + if node.right != nil && node.right.prefix.Overlaps(prefix) { + return search(prefix, node.right) + } + + return nil +} + +func (n *node) split() { + if n.isSplit() { + return + } + left, right := splitNetworkPrefix(n.prefix) + n.insert(leftDirection, left) + n.insert(rightDirection, right) +} + +func (n *node) merge() { + if n.left.isLeaf() && n.right.isLeaf() && !n.left.acquired && !n.right.acquired { + n.left = nil + n.right = nil + } +} + +func (n *node) insert(nd nodeDirection, prefix netip.Prefix) { + newNode := &node{prefix: prefix} + switch nd { + case leftDirection: + n.left = newNode + return + case rightDirection: + n.right = newNode + return + default: + return + } +} + +func (n *node) bestDirection() (first, second nodeDirection) { + if n.left.isSplit() { + return leftDirection, rightDirection + } + if n.right.isSplit() { + return rightDirection, leftDirection + } + return leftDirection, rightDirection +} + +func (n *node) isSplit() bool { + if n.left != nil && n.right != nil { + return true + } + return false +} + +func (n *node) isLeaf() bool { + if n.left == nil && n.right == nil { + return true + } + return false +} + +func (n *node) next(direction nodeDirection) *node { + switch direction { + case leftDirection: + return n.left + case rightDirection: + return n.right + default: + return nil + } +} + +func (n *node) toGraphviz(path string) error { + var sb strings.Builder + sb.WriteString("digraph G {\n") + n.toGraphvizRecursive(&sb) + sb.WriteString("}\n") + + if _, err := os.Stat(path + "/graphviz"); os.IsNotExist(err) { + if err := os.Mkdir(path+"/graphviz", 0o700); err != nil { + return err + } + } + + file, err := os.Create( + path + "/graphviz/" + strings.NewReplacer("/", "_", ".", "_").Replace(n.prefix.String()) + ".dot", + ) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(sb.String()) + return err +} + +func (n *node) toGraphvizRecursive(sb *strings.Builder) { + if n == nil { + return + } + label := n.prefix.String() + if len(n.ips) > 0 { + ipsString := []string{} + for i := range n.ips { + ipsString = append(ipsString, n.ips[i].String()) + } + label += "\\n" + strings.Join(ipsString, "\\n") + } + if n.acquired { + fmt.Fprintf(sb, " %q [label=\"%s\", style=filled, color=\"#57cc99\"];\n", n.prefix, label) + } + if n.left != nil { + fmt.Fprintf(sb, " %q -> %q;\n", n.prefix, n.left.prefix) + n.left.toGraphvizRecursive(sb) + } + if n.right != nil { + fmt.Fprintf(sb, " %q -> %q;\n", n.prefix, n.right.prefix) + n.right.toGraphvizRecursive(sb) + } +} diff --git a/pkg/ipam/initialize.go b/pkg/ipam/initialize.go index 647e04e8cf..36e3242b35 100644 --- a/pkg/ipam/initialize.go +++ b/pkg/ipam/initialize.go @@ -24,6 +24,8 @@ import ( // +kubebuilder:rbac:groups=ipam.liqo.io,resources=networks,verbs=get;list;watch func (lipam *LiqoIPAM) initialize(ctx context.Context) error { + klog.Info("Initializing IPAM") + if err := lipam.initializeNetworks(ctx); err != nil { return err } @@ -38,15 +40,14 @@ func (lipam *LiqoIPAM) initialize(ctx context.Context) error { func (lipam *LiqoIPAM) initializeNetworks(ctx context.Context) error { // List all networks present in the cluster. - nets, err := listNetworksOnCluster(ctx, lipam.Client) + nets, err := lipam.listNetworksOnCluster(ctx, lipam.Client) if err != nil { return err } // Initialize the networks. for _, net := range nets { - if err := lipam.reserveNetwork(net); err != nil { - klog.Errorf("Failed to reserve network %q: %v", net, err) + if _, err := lipam.acquireSpecificNetwork(net); err != nil { return err } } @@ -56,14 +57,14 @@ func (lipam *LiqoIPAM) initializeNetworks(ctx context.Context) error { func (lipam *LiqoIPAM) initializeIPs(ctx context.Context) error { // List all IPs present in the cluster. - ips, err := listIPsOnCluster(ctx, lipam.Client) + ips, err := lipam.listIPsOnCluster(ctx, lipam.Client) if err != nil { return err } // Initialize the IPs. for _, ip := range ips { - if err := lipam.reserveIP(ip); err != nil { + if _, err := lipam.acquireIPWithAddress(ip.cidr, ip.ip); err != nil { klog.Errorf("Failed to reserve IP %q (network %q): %v", ip.ip, ip.cidr, err) return err } diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index e4945f394d..d3839234a4 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -22,19 +22,20 @@ import ( "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "sigs.k8s.io/controller-runtime/pkg/client" + + ipamcore "github.com/liqotech/liqo/pkg/ipam/core" ) // LiqoIPAM is the struct implementing the IPAM interface. type LiqoIPAM struct { UnimplementedIPAMServer - HealthServer *health.Server - client.Client + IpamCore *ipamcore.Ipam + mutex sync.Mutex - opts *ServerOptions - cacheNetworks map[string]networkInfo - cacheIPs map[string]ipInfo - mutex sync.Mutex + HealthServer *health.Server + client.Client + opts *ServerOptions } // ServerOptions contains the options to configure the IPAM server. @@ -44,18 +45,21 @@ type ServerOptions struct { } // New creates a new instance of the LiqoIPAM. -func New(ctx context.Context, cl client.Client, opts *ServerOptions) (*LiqoIPAM, error) { +func New(ctx context.Context, cl client.Client, roots []string, opts *ServerOptions) (*LiqoIPAM, error) { hs := health.NewServer() hs.SetServingStatus(IPAM_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_NOT_SERVING) - lipam := &LiqoIPAM{ - HealthServer: hs, + ipam, err := ipamcore.NewIpam(roots, []string{}) + if err != nil { + return nil, err + } - Client: cl, + lipam := &LiqoIPAM{ + IpamCore: ipam, - opts: opts, - cacheNetworks: make(map[string]networkInfo), - cacheIPs: make(map[string]ipInfo), + HealthServer: hs, + Client: cl, + opts: opts, } // Initialize the IPAM instance @@ -90,9 +94,19 @@ func (lipam *LiqoIPAM) IPRelease(_ context.Context, req *IPReleaseRequest) (*IPR // NetworkAcquire acquires a network. If it is already reserved, it allocates and reserves a new free one with the same prefix length. func (lipam *LiqoIPAM) NetworkAcquire(_ context.Context, req *NetworkAcquireRequest) (*NetworkAcquireResponse, error) { - remappedCidr, err := lipam.acquireNetwork(req.GetCidr(), req.GetImmutable()) - if err != nil { - return &NetworkAcquireResponse{}, err + var remappedCidr string + var err error + + if req.GetImmutable() { + remappedCidr, err = lipam.acquireSpecificNetwork(req.GetCidr()) + if err != nil { + return &NetworkAcquireResponse{}, err + } + } else { + remappedCidr, err = lipam.acquireNetwork(req.GetCidr()) + if err != nil { + return &NetworkAcquireResponse{}, err + } } return &NetworkAcquireResponse{Cidr: remappedCidr}, nil diff --git a/pkg/ipam/ips.go b/pkg/ipam/ips.go index 5784cb7ef9..0d8484600f 100644 --- a/pkg/ipam/ips.go +++ b/pkg/ipam/ips.go @@ -16,9 +16,10 @@ package ipam import ( "context" + "fmt" + "net/netip" "time" - "github.com/google/nftables" klog "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,59 +40,61 @@ func (i ipCidr) String() string { return i.ip + "-" + i.cidr } -// reserveIP reserves an IP, saving it in the cache. -func (lipam *LiqoIPAM) reserveIP(ip ipCidr) error { +// acquireIP acquires an IP, eventually remapped if conflicts are found. +func (lipam *LiqoIPAM) acquireIP(cidr string) (string, error) { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - if lipam.cacheIPs == nil { - lipam.cacheIPs = make(map[string]ipInfo) - } - lipam.cacheIPs[ip.String()] = ipInfo{ - ipCidr: ip, - creationTimestamp: time.Now(), + prefix := netip.MustParsePrefix(cidr) + + result := lipam.IpamCore.AllocateIP(prefix) + if result == nil { + return "", fmt.Errorf("failed to reserve IP in network %q", cidr) } - klog.Infof("Reserved IP %q (network %q)", ip.ip, ip.cidr) - return nil + klog.Infof("Acquired IP %q (network %q)", result.String(), cidr) + return result.String(), nil } -// acquireIP acquires an IP, eventually remapped if conflicts are found. -func (lipam *LiqoIPAM) acquireIP(cidr string) (string, error) { +// acquireIpWithAddress acquires an IP with a specific address. +func (lipam *LiqoIPAM) acquireIPWithAddress(cidr, ip string) (string, error) { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - // TODO: implement real IP acquire logic - if lipam.cacheIPs == nil { - lipam.cacheIPs = make(map[string]ipInfo) - } - firstIP, _, err := nftables.NetFirstAndLastIP(cidr) - if err != nil { - return "", err - } - ip := ipCidr{ - ip: firstIP.String(), - cidr: cidr, - } - lipam.cacheIPs[ip.String()] = ipInfo{ - ipCidr: ip, - creationTimestamp: time.Now(), + prefix := netip.MustParsePrefix(cidr) + addr := netip.MustParseAddr(ip) + + result := lipam.IpamCore.AllocateIPWithAddr(prefix, addr) + if result == nil { + return "", fmt.Errorf("failed to reserve IP %q in network %q", ip, cidr) } - klog.Infof("Acquired IP %q (network %q)", ip.ip, ip.cidr) - return ip.ip, nil + klog.Infof("Acquired specific IP %q (%q)", result.String(), cidr) + return result.String(), nil } // freeIP frees an IP, removing it from the cache. func (lipam *LiqoIPAM) freeIP(ip ipCidr) { lipam.mutex.Lock() defer lipam.mutex.Unlock() - - delete(lipam.cacheIPs, ip.String()) + defer lipam.IpamCore.ToGraphviz() + + prefix := netip.MustParsePrefix(ip.cidr) + addr := netip.MustParseAddr(ip.ip) + result := lipam.IpamCore.FreeIP(prefix, addr) + if result == nil { + klog.Warningf("Failed to free IP %q (network %q)", ip.ip, ip.cidr) + return + } klog.Infof("Freed IP %q (network %q)", ip.ip, ip.cidr) } -func listIPsOnCluster(ctx context.Context, cl client.Client) ([]ipCidr, error) { +func (lipam *LiqoIPAM) listIPsOnCluster(ctx context.Context, cl client.Client) ([]ipCidr, error) { + lipam.mutex.Lock() + defer lipam.mutex.Unlock() + var ips []ipCidr var ipList ipamv1alpha1.IPList if err := cl.List(ctx, &ipList); err != nil { diff --git a/pkg/ipam/networks.go b/pkg/ipam/networks.go index d0c785d3c8..c56fe73ae7 100644 --- a/pkg/ipam/networks.go +++ b/pkg/ipam/networks.go @@ -16,7 +16,8 @@ package ipam import ( "context" - "time" + "fmt" + "net/netip" klog "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -24,53 +25,56 @@ import ( ipamv1alpha1 "github.com/liqotech/liqo/apis/ipam/v1alpha1" ) -type networkInfo struct { - cidr string - creationTimestamp time.Time -} - -// reserveNetwork reserves a network, saving it in the cache. -func (lipam *LiqoIPAM) reserveNetwork(cidr string) error { +// acquireNetwork acquires a network, eventually remapped if conflicts are found. +func (lipam *LiqoIPAM) acquireNetwork(cidr string) (string, error) { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - if lipam.cacheNetworks == nil { - lipam.cacheNetworks = make(map[string]networkInfo) - } - lipam.cacheNetworks[cidr] = networkInfo{ - cidr: cidr, - creationTimestamp: time.Now(), + prefix := netip.MustParsePrefix(cidr) + + result := lipam.IpamCore.AllocateNetworkWithPrefix(prefix) + if result == nil { + result = lipam.IpamCore.AllocateNetwork(prefix.Bits()) + if result == nil { + return "", fmt.Errorf("failed to reserve network %q", cidr) + } } - klog.Infof("Reserved network %q", cidr) - return nil + klog.Infof("Acquired network %q -> %q", cidr, result.String()) + return result.String(), nil } -// acquireNetwork acquires a network, eventually remapped if conflicts are found. -func (lipam *LiqoIPAM) acquireNetwork(cidr string, immutable bool) (string, error) { +// acquireSpecificNetwork acquires a network with a specific prefix. +// If the network is already allocated, it returns an error. +func (lipam *LiqoIPAM) acquireSpecificNetwork(cidr string) (string, error) { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - // TODO: implement real network acquire logic - _ = immutable - if lipam.cacheNetworks == nil { - lipam.cacheNetworks = make(map[string]networkInfo) - } - lipam.cacheNetworks[cidr] = networkInfo{ - cidr: cidr, - creationTimestamp: time.Now(), + prefix := netip.MustParsePrefix(cidr) + + result := lipam.IpamCore.AllocateNetworkWithPrefix(prefix) + if result == nil { + return "", fmt.Errorf("failed to reserve specific network %q", cidr) } - klog.Infof("Acquired network %q", cidr) - return cidr, nil + klog.Infof("Acquired specific network %q -> %q", cidr, result.String()) + return result.String(), nil } // freeNetwork frees a network, removing it from the cache. func (lipam *LiqoIPAM) freeNetwork(cidr string) { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - delete(lipam.cacheNetworks, cidr) + prefix := netip.MustParsePrefix(cidr) + + result := lipam.IpamCore.FreeNetwork(prefix) + if result == nil { + return + } klog.Infof("Freed network %q", cidr) } @@ -78,16 +82,14 @@ func (lipam *LiqoIPAM) freeNetwork(cidr string) { func (lipam *LiqoIPAM) isNetworkAvailable(cidr string) bool { lipam.mutex.Lock() defer lipam.mutex.Unlock() + defer lipam.IpamCore.ToGraphviz() - if lipam.cacheNetworks == nil { - return true - } - _, ok := lipam.cacheNetworks[cidr] + prefix := netip.MustParsePrefix(cidr) - return ok + return !lipam.IpamCore.IsAllocatedNetwork(prefix) } -func listNetworksOnCluster(ctx context.Context, cl client.Client) ([]string, error) { +func (lipam *LiqoIPAM) listNetworksOnCluster(ctx context.Context, cl client.Client) ([]string, error) { var nets []string var networks ipamv1alpha1.NetworkList if err := cl.List(ctx, &networks); err != nil { @@ -99,7 +101,6 @@ func listNetworksOnCluster(ctx context.Context, cl client.Client) ([]string, err cidr := net.Status.CIDR.String() if cidr == "" { - klog.Warningf("Network %q has no CIDR", net.Name) continue } diff --git a/pkg/ipam/sync.go b/pkg/ipam/sync.go index a8d061629b..49373afa50 100644 --- a/pkg/ipam/sync.go +++ b/pkg/ipam/sync.go @@ -16,6 +16,7 @@ package ipam import ( "context" + "net/netip" "os" "time" @@ -55,28 +56,28 @@ func (lipam *LiqoIPAM) sync(ctx context.Context, syncFrequency time.Duration) { func (lipam *LiqoIPAM) syncNetworks(ctx context.Context, expiredThreshold time.Time) error { // List all networks present in the cluster. - clusterNetworks, err := listNetworksOnCluster(ctx, lipam.Client) + clusterNetworks, err := lipam.listNetworksOnCluster(ctx, lipam.Client) if err != nil { return err } - // Create the set of networks present in the cluster (for faster lookup later). - setClusterNetworks := make(map[string]struct{}) - // Add networks that are present in the cluster but not in the cache. for _, net := range clusterNetworks { - if _, inCache := lipam.cacheNetworks[net]; !inCache { - if err := lipam.reserveNetwork(net); err != nil { - return err - } - } - setClusterNetworks[net] = struct{}{} // add network to the set + _, _ = lipam.acquireSpecificNetwork(net) } // Remove networks that are present in the cache but not in the cluster, and were added before the threshold. - for key := range lipam.cacheNetworks { - if _, inCluster := setClusterNetworks[key]; !inCluster && lipam.cacheNetworks[key].creationTimestamp.Before(expiredThreshold) { - lipam.freeNetwork(lipam.cacheNetworks[key].cidr) + currentNetworks := lipam.IpamCore.ListNetworks() + for i := range currentNetworks { + found := false + for j := range clusterNetworks { + if currentNetworks[i].String() == clusterNetworks[j] { + found = true + break + } + } + if !found { + lipam.freeNetwork(currentNetworks[i].String()) } } @@ -85,28 +86,34 @@ func (lipam *LiqoIPAM) syncNetworks(ctx context.Context, expiredThreshold time.T func (lipam *LiqoIPAM) syncIPs(ctx context.Context, expiredThreshold time.Time) error { // List all IPs present in the cluster. - ips, err := listIPsOnCluster(ctx, lipam.Client) + ips, err := lipam.listIPsOnCluster(ctx, lipam.Client) if err != nil { return err } - // Create the set of IPs present in the cluster (for faster lookup later). - setClusterIPs := make(map[string]struct{}) + for i := range ips { + _, _ = lipam.acquireIPWithAddress(ips[i].cidr, ips[i].ip) + } - // Add IPs that are present in the cluster but not in the cache. - for _, ip := range ips { - if _, inCache := lipam.cacheIPs[ip.String()]; !inCache { - if err := lipam.reserveIP(ip); err != nil { - return err - } - } - setClusterIPs[ip.String()] = struct{}{} // add IP to the set + ipMap := make(map[netip.Prefix][]netip.Addr) + + currentNetworks := lipam.IpamCore.ListNetworks() + for i := range currentNetworks { + ipMap[currentNetworks[i]] = lipam.IpamCore.ListIPs(currentNetworks[i]) } - // Remove IPs that are present in the cache but not in the cluster, and were added before the threshold. - for key := range lipam.cacheIPs { - if _, inCluster := setClusterIPs[key]; !inCluster && lipam.cacheIPs[key].creationTimestamp.Before(expiredThreshold) { - lipam.freeIP(lipam.cacheIPs[key].ipCidr) + for k := range ipMap { + for i := range ipMap[k] { + found := false + for j := range ips { + if ipMap[k][i].String() == ips[j].ip && k.String() == ips[j].cidr { + found = true + break + } + } + if !found { + lipam.freeIP(ipCidr{ip: ipMap[k][i].String(), cidr: k.String()}) + } } } diff --git a/pkg/ipam/sync_test.go b/pkg/ipam/sync_test.go index 4ff86ec37e..a8c2eccb15 100644 --- a/pkg/ipam/sync_test.go +++ b/pkg/ipam/sync_test.go @@ -15,159 +15,17 @@ package ipam import ( - "context" - "time" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - ipamv1alpha1 "github.com/liqotech/liqo/apis/ipam/v1alpha1" - networkingv1beta1 "github.com/liqotech/liqo/apis/networking/v1beta1" ) var _ = Describe("Sync routine tests", func() { - const ( - syncFrequency = 3 * time.Second - testNamespace = "test" - ) - - var ( - ctx context.Context - fakeClientBuilder *fake.ClientBuilder - now time.Time - newEntryThreshold time.Time - notNewTimestamp time.Time - - fakeIpamServer *LiqoIPAM - - addNetowrkToCache = func(ipamServer *LiqoIPAM, cidr string, creationTimestamp time.Time) { - ipamServer.cacheNetworks[cidr] = networkInfo{ - cidr: cidr, - creationTimestamp: creationTimestamp, - } - } - - addIPToCache = func(ipamServer *LiqoIPAM, ip, cidr string, creationTimestamp time.Time) { - ipC := ipCidr{ip: ip, cidr: cidr} - ipamServer.cacheIPs[ipC.String()] = ipInfo{ - ipCidr: ipC, - creationTimestamp: creationTimestamp, - } - } - - newNetwork = func(name, cidr string) *ipamv1alpha1.Network { - return &ipamv1alpha1.Network{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: testNamespace, - }, - Spec: ipamv1alpha1.NetworkSpec{ - CIDR: networkingv1beta1.CIDR(cidr), - }, - Status: ipamv1alpha1.NetworkStatus{ - CIDR: networkingv1beta1.CIDR(cidr), - }, - } - } - - newIP = func(name, ip, cidr string) *ipamv1alpha1.IP { - return &ipamv1alpha1.IP{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: testNamespace, - }, - Spec: ipamv1alpha1.IPSpec{ - IP: networkingv1beta1.IP(ip), - }, - Status: ipamv1alpha1.IPStatus{ - IP: networkingv1beta1.IP(ip), - CIDR: networkingv1beta1.CIDR(cidr), - }, - } - } - ) - - BeforeEach(func() { - ctx = context.Background() - fakeClientBuilder = fake.NewClientBuilder().WithScheme(scheme.Scheme) - now = time.Now() - newEntryThreshold = now.Add(-syncFrequency) - notNewTimestamp = newEntryThreshold.Add(-time.Minute) - }) - Describe("Testing the sync routine", func() { Context("Sync Networks", func() { - BeforeEach(func() { - // Add in-cluster networks - client := fakeClientBuilder.WithObjects( - newNetwork("net1", "10.0.0.0/16"), - newNetwork("net2", "10.1.0.0/16"), - newNetwork("net3", "10.2.0.0/16"), - ).Build() - - // Populate the cache - fakeIpamServer = &LiqoIPAM{ - Client: client, - cacheNetworks: make(map[string]networkInfo), - } - addNetowrkToCache(fakeIpamServer, "10.0.0.0/16", now) - addNetowrkToCache(fakeIpamServer, "10.1.0.0/16", notNewTimestamp) - addNetowrkToCache(fakeIpamServer, "10.3.0.0/16", notNewTimestamp) - addNetowrkToCache(fakeIpamServer, "10.4.0.0/16", now) - }) - It("should remove networks from cache if they are not present in the cluster", func() { - // Run sync - Expect(fakeIpamServer.syncNetworks(ctx, newEntryThreshold)).To(Succeed()) - - // Check the cache - Expect(fakeIpamServer.cacheNetworks).To(HaveKey("10.0.0.0/16")) // network in cluster and cache - Expect(fakeIpamServer.cacheNetworks).To(HaveKey("10.1.0.0/16")) // network in cluster and cache before new entry threshold - Expect(fakeIpamServer.cacheNetworks).To(HaveKey("10.2.0.0/16")) // network in cluster but not in cache - Expect(fakeIpamServer.cacheNetworks).NotTo(HaveKey("10.3.0.0/16")) // network not in cluster but in cache before new entry threshold - Expect(fakeIpamServer.cacheNetworks).To(HaveKey("10.4.0.0/16")) // network not in cluster but in cache after new entry threshold - }) }) Context("Sync IPs", func() { - BeforeEach(func() { - // Add in-cluster IPs - client := fakeClientBuilder.WithObjects( - newIP("ip1", "10.0.0.0", "10.0.0.0/24"), - newIP("ip2", "10.0.0.1", "10.0.0.0/24"), - newIP("ip3", "10.0.0.2", "10.0.0.0/24"), - ).Build() - - // Populate the cache - fakeIpamServer = &LiqoIPAM{ - Client: client, - cacheIPs: make(map[string]ipInfo), - } - addIPToCache(fakeIpamServer, "10.0.0.0", "10.0.0.0/24", now) - addIPToCache(fakeIpamServer, "10.0.0.1", "10.0.0.0/24", notNewTimestamp) - addIPToCache(fakeIpamServer, "10.0.0.3", "10.0.0.0/24", notNewTimestamp) - addIPToCache(fakeIpamServer, "10.0.0.4", "10.0.0.0/24", now) - }) - - It("should remove IPs from cache if they are not present in the cluster", func() { - // Run sync - Expect(fakeIpamServer.syncIPs(ctx, newEntryThreshold)).To(Succeed()) - // Check the cache - Expect(fakeIpamServer.cacheIPs).To(HaveKey( - ipCidr{ip: "10.0.0.0", cidr: "10.0.0.0/24"}.String())) // IP in cluster and cache - Expect(fakeIpamServer.cacheIPs).To(HaveKey( - ipCidr{ip: "10.0.0.1", cidr: "10.0.0.0/24"}.String())) // IP in cluster and cache before new entry threshold - Expect(fakeIpamServer.cacheIPs).To(HaveKey( - ipCidr{ip: "10.0.0.2", cidr: "10.0.0.0/24"}.String())) // IP in cluster but not in cache - Expect(fakeIpamServer.cacheIPs).NotTo(HaveKey( - ipCidr{ip: "10.0.0.3", cidr: "10.0.0.0/24"}.String())) // IP not in cluster but in cache before new entry threshold - Expect(fakeIpamServer.cacheIPs).To(HaveKey( - ipCidr{ip: "10.0.0.4", cidr: "10.0.0.0/24"}.String())) // IP not in cluster but in cache after new entry threshold - }) }) }) })