From 2f06f8bf384cd747a4956fac7b4c6a0182b1510e Mon Sep 17 00:00:00 2001 From: peppi-lotta Date: Tue, 20 Aug 2024 08:09:48 +0000 Subject: [PATCH] This commit makes it possible to: - Deploy IPAM with clusterctl - Reconsile CAPI's ipaddressclaims with this managers ippools Signed-off-by: peppi-lotta --- Dockerfile | 2 +- README.md | 2 +- config/default/kustomization.yaml | 7 +- config/manager/manager.yaml | 7 + config/rbac/role.yaml | 40 + controllers/ippool_controller.go | 39 +- controllers/ippool_controller_test.go | 60 + controllers/suite_test.go | 9 + docs/api.md | 29 +- examples/generate.sh | 2 +- examples/ippool/ippool.yaml | 10 + .../manager_tolerations_patch.yaml | 2 +- ipam/ippool_manager.go | 410 +++- ipam/ippool_manager_test.go | 2174 ++++++++++++++--- ipam/suite_test.go | 7 + main.go | 16 +- 16 files changed, 2396 insertions(+), 420 deletions(-) diff --git a/Dockerfile b/Dockerfile index a405cbca..b9071d15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG BUILD_IMAGE=docker.io/golang:1.22.6@sha256:d5e49f92b9566b0ddfc59a0d9d85cd8a8 ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f # Build the manager binary on golang image -FROM $BUILD_IMAGE as builder +FROM $BUILD_IMAGE AS builder WORKDIR /workspace # Run this with docker build --build_arg $(go env GOPROXY) to override the goproxy diff --git a/README.md b/README.md index 305ec9ed..a5e8e445 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Deploys IPAM CRDs and deploys IPAM controllers Runs IPAM controller locally ```sh - kubectl scale -n capm3-system \ + kubectl scale -n ipam-system \ deployment.v1.apps/metal3-ipam-controller-manager --replicas 0 make run ``` diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 960b1d76..497db424 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,16 +1,15 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -# Adds namespace to all resources. Keep it in capm3-system, as it is a -# dependency for CAPM3 -namespace: capm3-system +# Adds namespace to all resources. +namespace: ipam-system namePrefix: ipam- labels: - includeSelectors: true pairs: - cluster.x-k8s.io/provider: infrastructure-metal3 + cluster.x-k8s.io/provider: ipam-metal3 resources: - ../rbac diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 9f5b176c..54815c3c 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -1,3 +1,10 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index eacfa5dc..9bb2b01b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -53,6 +53,46 @@ rules: - clusters/status verbs: - get +- apiGroups: + - ipam.cluster.x-k8s.io + resources: + - ipaddressclaims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ipam.cluster.x-k8s.io + resources: + - ipaddressclaims/status + verbs: + - get + - patch + - update +- apiGroups: + - ipam.cluster.x-k8s.io + resources: + - ipaddresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ipam.cluster.x-k8s.io + resources: + - ipaddresses/status + verbs: + - get + - patch + - update - apiGroups: - ipam.metal3.io resources: diff --git a/controllers/ippool_controller.go b/controllers/ippool_controller.go index c0d919ce..c652ce36 100644 --- a/controllers/ippool_controller.go +++ b/controllers/ippool_controller.go @@ -27,6 +27,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util/annotations" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/predicates" @@ -55,6 +56,10 @@ type IPPoolReconciler struct { // +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipclaims/status,verbs=get;update;patch // +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipaddresses,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=ipam.metal3.io,resources=ipaddresses/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddresses/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch @@ -96,7 +101,7 @@ func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ c ipamv1IPPool.ObjectMeta.Labels = make(map[string]string) } ipamv1IPPool.ObjectMeta.Labels[clusterv1.ClusterNameLabel] = *ipamv1IPPool.Spec.ClusterName - ipamv1IPPool.ObjectMeta.Labels[clusterv1.ProviderNameLabel] = "infrastructure-metal3" + ipamv1IPPool.ObjectMeta.Labels[clusterv1.ProviderNameLabel] = "ipam-metal3" // Fetch the Cluster. Ignore an error if the deletion timestamp is set err = r.Client.Get(ctx, key, cluster) @@ -170,7 +175,7 @@ func (r *IPPoolReconciler) reconcileDelete(ctx context.Context, } // SetupWithManager will add watches for this controller. -func (r *IPPoolReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { +func (r *IPPoolReconciler) SetupWithManagerForIPClaim(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { return ctrl.NewControllerManagedBy(mgr). For(&ipamv1.IPPool{}). WithOptions(options). @@ -182,6 +187,19 @@ func (r *IPPoolReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manage Complete(r) } +// SetupWithManager will add watches for this controller. +func (r *IPPoolReconciler) SetupWithManagerForIPAddressClaim(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + return ctrl.NewControllerManagedBy(mgr). + For(&ipamv1.IPPool{}). + WithOptions(options). + Watches( + &capipamv1.IPAddressClaim{}, + handler.EnqueueRequestsFromMapFunc(r.IPAddressClaimToIPPool), + ). + WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). + Complete(r) +} + // IPClaimToIPPool will return a reconcile request for a // Metal3DataTemplate if the event is for a // IPClaim and that IPClaim references a Metal3DataTemplate. @@ -205,6 +223,23 @@ func (r *IPPoolReconciler) IPClaimToIPPool(_ context.Context, obj client.Object) return []ctrl.Request{} } +func (r *IPPoolReconciler) IPAddressClaimToIPPool(_ context.Context, obj client.Object) []ctrl.Request { + if ipac, ok := obj.(*capipamv1.IPAddressClaim); ok { + if ipac.Spec.PoolRef.Name != "" { + namespace := ipac.Namespace + return []ctrl.Request{ + { + NamespacedName: types.NamespacedName{ + Name: ipac.Spec.PoolRef.Name, + Namespace: namespace, + }, + }, + } + } + } + return []ctrl.Request{} +} + func checkRequeueError(err error, errMessage string) (ctrl.Result, error) { if err == nil { return ctrl.Result{}, nil diff --git a/controllers/ippool_controller_test.go b/controllers/ippool_controller_test.go index a0456b86..2a2951b1 100644 --- a/controllers/ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -33,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -407,4 +408,63 @@ var _ = Describe("IPPool controller", func() { }, ), ) + + type TestCaseK8SIPACToM3IPP struct { + IPAddressClaim *capipamv1.IPAddressClaim + ExpectRequest bool + } + + DescribeTable("IPAddressClaim To IPPool tests", + func(tc TestCaseK8SIPACToM3IPP) { + r := IPPoolReconciler{} + obj := client.Object(tc.IPAddressClaim) + reqs := r.IPAddressClaimToIPPool(context.Background(), obj) + + if tc.ExpectRequest { + Expect(len(reqs)).To(Equal(1), "Expected 1 request, found %d", len(reqs)) + + req := reqs[0] + Expect(req.NamespacedName.Name).To(Equal(tc.IPAddressClaim.Spec.PoolRef.Name), + "Expected name %s, found %s", tc.IPAddressClaim.Spec.PoolRef.Name, req.NamespacedName.Name) + } else { + Expect(len(reqs)).To(Equal(0), "Expected 0 request, found %d", len(reqs)) + + } + }, + Entry("No IPPool in Spec", + TestCaseK8SIPACToM3IPP{ + IPAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: testObjectMeta, + Spec: capipamv1.IPAddressClaimSpec{}, + }, + ExpectRequest: false, + }, + ), + Entry("IPPool in Spec, with namespace", + TestCaseK8SIPACToM3IPP{ + IPAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: testObjectMeta, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "abc", + }, + }, + }, + ExpectRequest: true, + }, + ), + Entry("IPPool in Spec, no namespace", + TestCaseK8SIPACToM3IPP{ + IPAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: testObjectMeta, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "abc", + }, + }, + }, + ExpectRequest: true, + }, + ), + ) }) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 9498fb10..c9182df1 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -32,6 +32,7 @@ import ( ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -57,6 +58,7 @@ func init() { _ = apiextensionsv1.AddToScheme(scheme.Scheme) _ = clusterv1.AddToScheme(scheme.Scheme) _ = ipamv1.AddToScheme(scheme.Scheme) + _ = capipamv1.AddToScheme(scheme.Scheme) } func setupScheme() *runtime.Scheme { @@ -70,6 +72,10 @@ func setupScheme() *runtime.Scheme { panic(err) } + if err := capipamv1.AddToScheme(s); err != nil { + panic(err) + } + return s } func TestAPIs(t *testing.T) { @@ -94,6 +100,9 @@ var _ = BeforeSuite(func() { err = ipamv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = capipamv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = apiextensionsv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) diff --git a/docs/api.md b/docs/api.md index 61401bbc..4208e25e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -56,7 +56,7 @@ as follows : An IPClaim is an object representing a request for an IP address allocation. -Example pool: +Example IPClaim: ```yaml apiVersion: ipam.metal3.io/v1alpha1 @@ -78,7 +78,7 @@ The *spec* field contains the following : An IPAddress is an object representing an IP address allocation. -Example pool: +Example IPAddress: ```yaml apiVersion: ipam.metal3.io/v1alpha1 @@ -110,3 +110,28 @@ The *spec* field contains the following : You can find CR examples in the [Metal3-io dev env project](https://github.com/metal3-io/metal3-dev-env) + +## CAPI + +[Cluster-api](https://github.com/kubernetes-sigs/cluster-api) has +created it own ip-address-manager: +[ipam-provider-in-cluster](https://github.com/kubernetes-sigs/cluster-api-ipam-provider-in-cluster) +and support for other IPAMs. This IPAM can be deployed and used +as an IPAM provider for CAPI. + +Deploying an ippool like the example pool will be able to reconsile (metal3)ipclaims into (metal3)ipaddresses and (capi)ipaddressclaims into (capi)ipaddresses. + +### IPAddressClaim + +### IpAddress + +### Set up via clusterctl + +Since it's not added to the built-in list of providers yet, you'll need to add the following to your ```$XDG_CONFIG_HOME/cluster-api/clusterctl.yaml``` if you want to install it using ```clusterctl init --ipam metal3```: + +``` +providers: + - name: metal3 + url: file:///home/ipam/ipam-metal3/v1.0.0/ipam-components.yaml + type: IPAMProvider +``` diff --git a/examples/generate.sh b/examples/generate.sh index 294fc762..180a9190 100755 --- a/examples/generate.sh +++ b/examples/generate.sh @@ -23,7 +23,7 @@ OUTPUT_DIR=${OUTPUT_DIR:-${SOURCE_DIR}/_out} # Cluster. export CLUSTER_NAME="${CLUSTER_NAME:-test1}" -export NAMESPACE="${NAMESPACE:-capm3-system}" +export NAMESPACE="${NAMESPACE:-ipam-system}" # Outputs. COMPONENTS_CERT_MANAGER_GENERATED_FILE=${OUTPUT_DIR}/cert-manager.yaml diff --git a/examples/ippool/ippool.yaml b/examples/ippool/ippool.yaml index 5a323d02..d7603b35 100644 --- a/examples/ippool/ippool.yaml +++ b/examples/ippool/ippool.yaml @@ -39,3 +39,13 @@ spec: pool: name: pool1 namespace: ${NAMESPACE} +--- +apiVersion: ipam.cluster.x-k8s.io/v1beta1 +kind: IPAddressClaim +metadata: + name: ${CLUSTER_NAME}-controlplane-template-1-provisioning-pool +spec: + poolRef: + apiGroup: ipam.metal3.io + kind: IPPool + name: pool1 diff --git a/examples/provider-components/manager_tolerations_patch.yaml b/examples/provider-components/manager_tolerations_patch.yaml index bc38afcb..90cf1386 100644 --- a/examples/provider-components/manager_tolerations_patch.yaml +++ b/examples/provider-components/manager_tolerations_patch.yaml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: ipam-controller-manager - namespace: capm3-system + namespace: ipam-system spec: template: spec: diff --git a/ipam/ippool_manager.go b/ipam/ippool_manager.go index 3a91f847..b060aa03 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -29,11 +29,20 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" ) -var notFoundErr *NotFoundError +var ( + notFoundErr *NotFoundError + APIGroup = "ipam.metal3.io" +) + +const ( + IPAddressClaimFinalizer = "ipam.metal3.io/ipaddressclaim" + IPAddressFinalizer = "ipam.metal3.io/ipaddress" +) // IPPoolManagerInterface is an interface for a IPPoolManager. type IPPoolManagerInterface interface { @@ -154,6 +163,33 @@ func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr addresses[addressObject.Spec.Address] = claimName } + // get list of IPAddress objects for cluster.x-k8s.io addresses + K8sAddressObjects := capipamv1.IPAddressList{} + err = m.client.List(ctx, &K8sAddressObjects, opts) + if err != nil { + return addresses, err + } + + // Iterate over the IPAddress objects to find all addresses and objects + for _, addressObject := range K8sAddressObjects.Items { + // If IPPool does not point to this object, discard + if addressObject.Spec.PoolRef.Name == "" { + continue + } + if addressObject.Spec.PoolRef.Name != m.IPPool.Name { + continue + } + + // Get the claim Name, if unset use empty string, to still record the + // index being used, to avoid conflicts + claimName := "" + if addressObject.Spec.ClaimRef.Name != "" { + claimName = addressObject.Spec.ClaimRef.Name + } + updatedAllocations[claimName] = ipamv1.IPAddressStr(addressObject.Spec.Address) + addresses[ipamv1.IPAddressStr(addressObject.Spec.Address)] = claimName + } + if !reflect.DeepEqual(updatedAllocations, m.IPPool.Status.Allocations) { m.IPPool.Status.Allocations = updatedAllocations m.updateStatusTimestamp() @@ -170,6 +206,21 @@ func (m *IPPoolManager) updateStatusTimestamp() { // UpdateAddresses manages the claims and creates or deletes IPAddress accordingly. // It returns the number of current allocations. func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { + _, err := m.M3UpdateAddresses(ctx) + if err != nil { + return 0, err + } + count, err := m.K8sUpdateAddresses(ctx) + if err != nil { + return 0, err + } + + return count, nil +} + +// UpdateM3Addresses manages the ipclaims.ipam.metal3.io and creates or deletes IPAddress.ipam.metal3.io accordingly. +// It returns the number of current allocations. +func (m *IPPoolManager) M3UpdateAddresses(ctx context.Context) (int, error) { addresses, err := m.getIndexes(ctx) if err != nil { return 0, err @@ -207,6 +258,45 @@ func (m *IPPoolManager) UpdateAddresses(ctx context.Context) (int, error) { return len(addresses), nil } +// UpdateCAPIAddresses manages the ipaddressclaims.ipam.cluster.x-k8s.io and creates or deletes IPAddress.ipam.cluster.x-k8s.io accordingly. +// It returns the number of current allocations. +func (m *IPPoolManager) K8sUpdateAddresses(ctx context.Context) (int, error) { + addresses, err := m.getIndexes(ctx) + if err != nil { + return 0, err + } + // get list of IPClaim objects + addressClaimObjects := capipamv1.IPAddressClaimList{} + // without this ListOption, all namespaces would be including in the listing + opts := &client.ListOptions{ + Namespace: m.IPPool.Namespace, + } + + err = m.client.List(ctx, &addressClaimObjects, opts) + if err != nil { + return 0, err + } + + // Iterate over the IPAddressClaim objects to find all addresses and objects + for _, addressClaim := range addressClaimObjects.Items { + addressClaim := addressClaim + // If IPPool does not point to this object, discard + if addressClaim.Spec.PoolRef.Name != m.IPPool.Name { + continue + } + + if addressClaim.Status.AddressRef.Name != "" && addressClaim.DeletionTimestamp.IsZero() { + continue + } + addresses, err = m.K8sUpdateAddress(ctx, &addressClaim, addresses) + if err != nil { + return 0, err + } + } + m.updateStatusTimestamp() + return len(addresses), nil +} + func (m *IPPoolManager) updateAddress(ctx context.Context, addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (map[ipamv1.IPAddressStr]string, error) { @@ -247,6 +337,55 @@ func (m *IPPoolManager) updateAddress(ctx context.Context, return addresses, nil } +func (m *IPPoolManager) K8sUpdateAddress(ctx context.Context, + addressClaim *capipamv1.IPAddressClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + helper, err := patch.NewHelper(addressClaim, m.client) + if err != nil { + return addresses, errors.Wrap(err, "failed to init patch helper") + } + // Always patch addressClaim exiting this function so we can persist any changes. + defer func() { + err := helper.Patch(ctx, addressClaim) + if err != nil { + m.Log.Error(err, "failed to Patch IPAddressClaim") + } + }() + + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Severity: "Info", + Reason: "ErrorMessage", + Message: "", + }) + addressClaim.SetConditions(conditions) + + if addressClaim.DeletionTimestamp.IsZero() { + addresses, err = m.K8sCreateAddress(ctx, addressClaim, addresses) + if err != nil { + return addresses, err + } + } else { + // Check if this claim is in use. Does it have any other finalizers than our own? + // If it is no longer in use, proceed to delete the associated IPAddress + if len(addressClaim.Finalizers) > 1 || + (len(addressClaim.Finalizers) == 1 && !Contains(addressClaim.Finalizers, IPAddressClaimFinalizer)) { + m.Log.Info("IPAddressClaim is still in use (has other finalizers). Cannot delete IPAddress.", + "IPAddressClaim", addressClaim.Name, "Finalizers", addressClaim.Finalizers) + return addresses, nil + } + + addresses, err = m.K8sDeleteAddress(ctx, addressClaim, addresses) + if err != nil { + return addresses, err + } + } + return addresses, nil +} + func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, []ipamv1.IPAddressStr, error) { @@ -315,6 +454,88 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, return allocatedAddress, prefix, gateway, dnsServers, nil } +func (m *IPPoolManager) K8sAllocateAddress(addressClaim *capipamv1.IPAddressClaim, + addresses map[ipamv1.IPAddressStr]string, +) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, error) { + var allocatedAddress ipamv1.IPAddressStr + var err error + + // Get pre-allocated addresses + preAllocatedAddress, ipPreAllocated := m.IPPool.Spec.PreAllocations[addressClaim.Name] + // If the IP is pre-allocated, the default prefix and gateway are used + prefix := m.IPPool.Spec.Prefix + gateway := m.IPPool.Spec.Gateway + + ipAllocated := false + + for _, pool := range m.IPPool.Spec.Pools { + if ipAllocated { + break + } + index := 0 + for !ipAllocated { + allocatedAddress, err = ipamv1.GetIPAddress(pool, index) + if err != nil { + break + } + index++ + // We have a pre-allocated ip, we just need to ensure that it matches the current address + // if it does not, continue and try the next address + if ipPreAllocated && allocatedAddress != preAllocatedAddress { + continue + } + // Here the two addresses match, so we continue with that one + if ipPreAllocated { + ipAllocated = true + } + // If we have a preallocated address, this is useless, otherwise, check if the + // ip is free + if _, ok := addresses[allocatedAddress]; !ok && allocatedAddress != "" { + ipAllocated = true + } + if !ipAllocated { + continue + } + + if pool.Prefix != 0 { + prefix = pool.Prefix + } + if pool.Gateway != nil { + gateway = pool.Gateway + } + } + } + // We have a preallocated IP but we did not find it in the pools! It means it is + // misconfigured + if !ipAllocated && ipPreAllocated { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Pre-allocated IP out of bond", + }) + addressClaim.SetConditions(conditions) + return "", 0, nil, errors.New("Pre-allocated IP out of bond") + } + if !ipAllocated { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Exhausted IP Pools", + }) + addressClaim.SetConditions(conditions) + return "", 0, nil, errors.New("Exhausted IP Pools") + } + return allocatedAddress, prefix, gateway, nil +} + func (m *IPPoolManager) createAddress(ctx context.Context, addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (map[ipamv1.IPAddressStr]string, error) { @@ -413,6 +634,117 @@ func (m *IPPoolManager) createAddress(ctx context.Context, return addresses, nil } +func (m *IPPoolManager) K8sCreateAddress(ctx context.Context, + addressClaim *capipamv1.IPAddressClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + if !Contains(addressClaim.Finalizers, IPAddressClaimFinalizer) { + addressClaim.Finalizers = append(addressClaim.Finalizers, + IPAddressClaimFinalizer, + ) + } + + if allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name]; ok { + addressClaim.Status.AddressRef = corev1.LocalObjectReference{ + Name: m.formatAddressName(allocatedAddress), + } + return addresses, nil + } + + // Get a new index for this machine + m.Log.Info("Getting address", "Claim", addressClaim.Name) + // Get a new IP for this owner + allocatedAddress, prefix, gateway, err := m.K8sAllocateAddress(addressClaim, addresses) + if err != nil { + return addresses, err + } + + var gatewayStr string + if gateway != nil { + gatewayStr = string(*gateway) + } else { + gatewayStr = "" + } + + // Set the index and IPAddress names + addressName := m.formatAddressName(allocatedAddress) + + m.Log.Info("Address allocated", "Claim", addressClaim.Name, "address", allocatedAddress) + + ownerRefs := addressClaim.OwnerReferences + ownerRefs = append(ownerRefs, + metav1.OwnerReference{ + APIVersion: m.IPPool.APIVersion, + Kind: m.IPPool.Kind, + Name: m.IPPool.Name, + UID: m.IPPool.UID, + }, + metav1.OwnerReference{ + APIVersion: addressClaim.APIVersion, + Kind: addressClaim.Kind, + Name: addressClaim.Name, + UID: addressClaim.UID, + }, + ) + + // Create the IPAddress object, with an Owner ref to the IPAddressClaim, + // the IPPool, and the IPAddressClaim owners. Also add a finalizer. + addressObject := &capipamv1.IPAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: "IPAddress", + APIVersion: capipamv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: addressName, + Namespace: m.IPPool.Namespace, + Finalizers: []string{IPAddressFinalizer}, + OwnerReferences: ownerRefs, + Labels: addressClaim.Labels, + }, + Spec: capipamv1.IPAddressSpec{ + Address: string(allocatedAddress), + PoolRef: corev1.TypedLocalObjectReference{ + Name: m.IPPool.Name, + Kind: m.IPPool.Kind, + APIGroup: &APIGroup, + }, + ClaimRef: corev1.LocalObjectReference{ + Name: addressClaim.Name, + }, + Prefix: prefix, + Gateway: gatewayStr, + }, + } + + // Create the IPAddress object. If we get a conflict (that will set + // HasRequeueAfterError), then requeue to retrigger the reconciliation with + // the new state + if err := createObject(ctx, m.client, addressObject); err != nil { + var reqAfter *RequeueAfterError + if ok := errors.As(err, &reqAfter); !ok { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Failed to create associated IPAddress object", + }) + addressClaim.SetConditions(conditions) + } + return addresses, err + } + + m.IPPool.Status.Allocations[addressClaim.Name] = allocatedAddress + addresses[allocatedAddress] = addressClaim.Name + + addressClaim.Status.AddressRef = corev1.LocalObjectReference{ + Name: addressName, + } + + return addresses, nil +} + // deleteAddress removes the finalizer from the IPClaim and deletes the associated IPAddress. func (m *IPPoolManager) deleteAddress(ctx context.Context, addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, @@ -471,6 +803,82 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, return addresses, nil } +// deleteAddress removes the finalizer from the IPClaim and deletes the associated IPAddress. +func (m *IPPoolManager) K8sDeleteAddress(ctx context.Context, + addressClaim *capipamv1.IPAddressClaim, addresses map[ipamv1.IPAddressStr]string, +) (map[ipamv1.IPAddressStr]string, error) { + m.Log.Info("Deleting IPAddress associated with IPAddressClaim", "IPAddressClaim", addressClaim.Name) + + allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name] + if ok { + // Try to get the IPAddress. if it succeeds, delete it + ipAddress := &capipamv1.IPAddress{} + key := client.ObjectKey{ + Name: m.formatAddressName(allocatedAddress), + Namespace: m.IPPool.Namespace, + } + err := m.client.Get(ctx, key, ipAddress) + if err != nil && !apierrors.IsNotFound(err) { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Failed to get associated IPAddress object", + }) + addressClaim.SetConditions(conditions) + return addresses, err + } else if err == nil { + // Remove the finalizer + ipAddress.Finalizers = Filter(ipAddress.Finalizers, + IPAddressFinalizer, + ) + err = updateObject(ctx, m.client, ipAddress) + if err != nil && !apierrors.IsNotFound(err) { + m.Log.Info("Unable to remove finalizer from IPAddress", "IPAddress", ipAddress.Name) + return addresses, err + } + // Delete the IPAddress + err = deleteObject(ctx, m.client, ipAddress) + if err != nil { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Failed to delete associated IPAddress object", + }) + addressClaim.SetConditions(conditions) + return addresses, err + } + m.Log.Info("Deleted IPAddress", "IPAddress", ipAddress.Name) + } + } + addressClaim.Status.AddressRef.Name = "" + addressClaim.Finalizers = Filter(addressClaim.Finalizers, + IPAddressClaimFinalizer, + ) + err := updateObject(ctx, m.client, addressClaim) + if err != nil && !apierrors.IsNotFound(err) { + m.Log.Info("Unable to remove finalizer from IPAddressClaim", "IPAddressClaim", addressClaim.Name) + return addresses, err + } + + if ok { + if _, ok := m.IPPool.Spec.PreAllocations[addressClaim.Name]; !ok { + delete(addresses, allocatedAddress) + } + delete(m.IPPool.Status.Allocations, addressClaim.Name) + m.Log.Info("IPAddressClaim removed from IPPool allocations", "IPAddressClaim", addressClaim.Name) + } + m.updateStatusTimestamp() + return addresses, nil +} + // formatAddressName renders the name of the IPAddress objects. func (m *IPPoolManager) formatAddressName(address ipamv1.IPAddressStr) string { return strings.TrimRight(m.IPPool.Spec.NamePrefix+"-"+strings.Replace( diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 0ba20ab6..b4177ddc 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -41,6 +42,12 @@ var ( testObjectReference = &corev1.ObjectReference{ Name: "abc", } + localtestObjectReference = &corev1.LocalObjectReference{ + Name: "abc", + } + typedtestObjectReference = &corev1.TypedLocalObjectReference{ + Name: "abc", + } ) var _ = Describe("IPPool manager", func() { @@ -150,6 +157,7 @@ var _ = Describe("IPPool manager", func() { type testGetIndexes struct { ipPool *ipamv1.IPPool addresses []*ipamv1.IPAddress + capiAddresses []*capipamv1.IPAddress expectError bool expectedAddresses map[ipamv1.IPAddressStr]string expectedAllocations map[string]ipamv1.IPAddressStr @@ -161,6 +169,9 @@ var _ = Describe("IPPool manager", func() { for _, address := range tc.addresses { objects = append(objects, address) } + for _, address := range tc.capiAddresses { + objects = append(objects, address) + } c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build() ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, logr.Discard(), @@ -263,282 +274,342 @@ var _ = Describe("IPPool manager", func() { "abc": ipamv1.IPAddressStr("abcd1"), }, }), - Entry("IPPool with deletion timestamp", testGetIndexes{ + Entry("capi addresses", testGetIndexes{ ipPool: &ipamv1.IPPool{ - ObjectMeta: metav1.ObjectMeta{ - DeletionTimestamp: &timeNow, - }, + ObjectMeta: testObjectMeta, Spec: ipamv1.IPPoolSpec{ PreAllocations: map[string]ipamv1.IPAddressStr{ - "bcd": ipamv1.IPAddressStr("bcde"), + "cbcd": "cbcde", }, }, }, - addresses: []*ipamv1.IPAddress{ + capiAddresses: []*capipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-1-11", + Name: "cabc-0", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd1", + PoolRef: *typedtestObjectReference, + ClaimRef: *localtestObjectReference, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cbbc-1", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd2", + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cbbc", }, - Claim: corev1.ObjectReference{ - Name: "inUseClaim", - Namespace: "myns", + ClaimRef: corev1.LocalObjectReference{ + Name: "cbbc", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - Prefix: 24, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-2", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, }, }, }, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - }), - ) - - var ipPoolMeta = metav1.ObjectMeta{ - Name: "abc", - Namespace: "myns", - } - - type testCaseUpdateAddresses struct { - ipPool *ipamv1.IPPool - ipClaims []*ipamv1.IPClaim - ipAddresses []*ipamv1.IPAddress - expectRequeue bool - expectError bool - expectedNbAllocations int - expectedAllocations map[string]ipamv1.IPAddressStr - } - - DescribeTable("Test UpdateAddresses", - func(tc testCaseUpdateAddresses) { - objects := []client.Object{} - for _, address := range tc.ipAddresses { - objects = append(objects, address) - } - for _, claim := range tc.ipClaims { - objects = append(objects, claim) - } - c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithStatusSubresource(objects...).WithObjects(objects...).Build() - ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, - logr.Discard(), - ) - Expect(err).NotTo(HaveOccurred()) - - nbAllocations, err := ipPoolMgr.UpdateAddresses(context.TODO()) - if tc.expectRequeue || tc.expectError { - Expect(err).To(HaveOccurred()) - if tc.expectRequeue { - Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) - } else { - Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) - } - } else { - Expect(err).NotTo(HaveOccurred()) - } - Expect(nbAllocations).To(Equal(tc.expectedNbAllocations)) - Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) - Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) - - // get list of IPAddress objects - addressObjects := ipamv1.IPClaimList{} - opts := &client.ListOptions{} - err = c.List(context.TODO(), &addressObjects, opts) - Expect(err).NotTo(HaveOccurred()) - - // Iterate over the IPAddress objects to find all indexes and objects - for _, claim := range addressObjects.Items { - if claim.DeletionTimestamp.IsZero() { - Expect(claim.Status.Address).NotTo(BeNil()) - } - } - - }, - Entry("No Claims", testCaseUpdateAddresses{ - ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + "cabcd1": "abc", + "cbcde": "", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "cabcd1", }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, }), - Entry("Claim and IP exist", testCaseUpdateAddresses{ + Entry("metal3 addresses and capi addresses", testGetIndexes{ ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, + ObjectMeta: testObjectMeta, Spec: ipamv1.IPPoolSpec{ - NamePrefix: "abcpref", + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": "bcde", + "cbcd": "cbcde", + }, }, }, - ipClaims: []*ipamv1.IPClaim{ + addresses: []*ipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ - Name: "abc", + Name: "abc-0", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", - }, - }, - Status: ipamv1.IPClaimStatus{ - Address: &corev1.ObjectReference{ - Name: "abcpref-192-168-1-11", - Namespace: "myns", - }, + Spec: ipamv1.IPAddressSpec{ + Address: "abcd1", + Pool: *testObjectReference, + Claim: *testObjectReference, }, }, { ObjectMeta: metav1.ObjectMeta{ - Name: "abcd", + Name: "bbc-1", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv1.IPAddressSpec{ + Address: "abcd2", Pool: corev1.ObjectReference{ - Name: "abcd", + Name: "bbc", Namespace: "myns", }, - }, - Status: ipamv1.IPClaimStatus{ - Address: &corev1.ObjectReference{ - Name: "abcpref-192-168-1-12", + Claim: corev1.ObjectReference{ + Name: "bbc", Namespace: "myns", }, }, }, { ObjectMeta: metav1.ObjectMeta{ - Name: "abce", + Name: "abc-2", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv1.IPAddressSpec{ + Address: "abcd3", + Pool: corev1.ObjectReference{}, + Claim: *testObjectReference, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-3", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Address: "abcd4", Pool: corev1.ObjectReference{ - Name: "abc", Namespace: "myns", }, + Claim: corev1.ObjectReference{}, }, - Status: ipamv1.IPClaimStatus{ - Address: &corev1.ObjectReference{ - Name: "abcpref-192-168-1-12", - Namespace: "myns", + }, + }, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cbbc-1", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd2", + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cbbc", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "cbbc", }, }, }, { ObjectMeta: metav1.ObjectMeta{ - Name: "abcf", - Namespace: "myns", - DeletionTimestamp: &timeNow, - Finalizers: []string{ - ipamv1.IPClaimFinalizer, - }, + Name: "cabc-2", + Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", - }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, }, - Status: ipamv1.IPClaimStatus{ - Address: &corev1.ObjectReference{ - Name: "abcpref-192-168-1-13", - Namespace: "myns", - }, + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + "abcd1": "abc", + "bcde": "", + "cbcde": "", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "abcd1", + }, + }), + Entry("metal3 addresses and capi addresses 2", testGetIndexes{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv1.IPPoolSpec{ + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": "bcde", + "cbcd": "cbcde", }, }, }, - ipAddresses: []*ipamv1.IPAddress{ + addresses: []*ipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-1-11", + Name: "bbc-1", Namespace: "myns", }, Spec: ipamv1.IPAddressSpec{ + Address: "abcd2", Pool: corev1.ObjectReference{ - Name: "abc", + Name: "bbc", Namespace: "myns", }, Claim: corev1.ObjectReference{ - Name: "abc", + Name: "bbc", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - Prefix: 24, }, }, { ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-1-12", + Name: "abc-2", Namespace: "myns", }, Spec: ipamv1.IPAddressSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", - }, - Claim: corev1.ObjectReference{ - Name: "abce", - Namespace: "myns", - }, - Address: ipamv1.IPAddressStr("192.168.1.12"), + Address: "abcd3", + Pool: corev1.ObjectReference{}, + Claim: *testObjectReference, }, }, { ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-1-13", + Name: "abc-3", Namespace: "myns", }, Spec: ipamv1.IPAddressSpec{ + Address: "abcd4", Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", - }, - Claim: corev1.ObjectReference{ - Name: "abcf", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.13"), + Claim: corev1.ObjectReference{}, }, }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.1.11"), - "abce": ipamv1.IPAddressStr("192.168.1.12"), - }, - expectedNbAllocations: 2, - }), - Entry("IPClaim with deletion timestamp and finalizers", testCaseUpdateAddresses{ - ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - NamePrefix: "abcpref", + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-0", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd1", + PoolRef: *typedtestObjectReference, + ClaimRef: *localtestObjectReference, + }, }, - Status: ipamv1.IPPoolStatus{}, - }, - ipClaims: []*ipamv1.IPClaim{ { ObjectMeta: metav1.ObjectMeta{ - Name: "inUseClaim", - Namespace: "myns", - DeletionTimestamp: &timeNow, - Finalizers: []string{ - ipamv1.IPClaimFinalizer, - "metal3data.infrastructure.cluster.x-k8s.io", + Name: "cbbc-1", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd2", + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cbbc", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "cbbc", }, }, - Spec: ipamv1.IPClaimSpec{ + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-2", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, + }, + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + "bcde": "", + "cabcd1": "abc", + "cbcde": "", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "cabcd1", + }, + }), + Entry("IPPool with deletion timestamp (metal3 addresses)", testGetIndexes{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &timeNow, + }, + Spec: ipamv1.IPPoolSpec{ + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": ipamv1.IPAddressStr("bcde"), + }, + }, + }, + addresses: []*ipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, + Claim: corev1.ObjectReference{ + Name: "inUseClaim", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.11"), + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Prefix: 24, }, }, }, - ipAddresses: []*ipamv1.IPAddress{ + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + }), + Entry("IPPool with deletion timestamp (capi addresses)", testGetIndexes{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &timeNow, + }, + Spec: ipamv1.IPPoolSpec{ + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": "bcde", + }, + }, + }, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "inUseClaim", + }, + Address: "192.168.1.11", + Gateway: *ptr.To("192.168.0.1"), + Prefix: 24, + }, + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + }), + Entry("IPPool with deletion timestamp (metal3 and capi addresses)", testGetIndexes{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &timeNow, + }, + Spec: ipamv1.IPPoolSpec{ + PreAllocations: map[string]ipamv1.IPAddressStr{ + "bcd": "bcde", + }, + }, + }, + addresses: []*ipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-11", @@ -553,46 +624,75 @@ var _ = Describe("IPPool manager", func() { Name: "inUseClaim", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), + Address: "192.168.1.11", Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), Prefix: 24, }, }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "inUseClaim": ipamv1.IPAddressStr("192.168.1.11"), + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcpref-192-168-1-12", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cabc", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "inUseClaim", + }, + Address: "192.168.1.12", + Gateway: *(ptr.To("192.168.0.1")), + Prefix: 24, + }, + }, }, - expectedNbAllocations: 1, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, }), ) - type testCaseCreateAddresses struct { - ipPool *ipamv1.IPPool - ipClaim *ipamv1.IPClaim - ipAddresses []*ipamv1.IPAddress - addresses map[ipamv1.IPAddressStr]string - expectRequeue bool - expectError bool - expectedIPAddresses []string - expectedAddresses map[ipamv1.IPAddressStr]string - expectedAllocations map[string]ipamv1.IPAddressStr + var ipPoolMeta = metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", } - DescribeTable("Test CreateAddresses", - func(tc testCaseCreateAddresses) { + type testCaseUpdateAddresses struct { + ipPool *ipamv1.IPPool + ipClaims []*ipamv1.IPClaim + ipAddresses []*ipamv1.IPAddress + ipAddressClaims []*capipamv1.IPAddressClaim + capiAddresses []*capipamv1.IPAddress + expectRequeue bool + expectError bool + expectedNbAllocations int + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test UpdateAddresses", + func(tc testCaseUpdateAddresses) { objects := []client.Object{} for _, address := range tc.ipAddresses { objects = append(objects, address) } - c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build() + for _, claim := range tc.ipClaims { + objects = append(objects, claim) + } + for _, address := range tc.capiAddresses { + objects = append(objects, address) + } + for _, claim := range tc.ipAddressClaims { + objects = append(objects, claim) + } + c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithStatusSubresource(objects...).WithObjects(objects...).Build() ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, logr.Discard(), ) Expect(err).NotTo(HaveOccurred()) - allocatedMap, err := ipPoolMgr.createAddress(context.TODO(), tc.ipClaim, - tc.addresses, - ) + nbAllocations, err := ipPoolMgr.UpdateAddresses(context.TODO()) if tc.expectRequeue || tc.expectError { Expect(err).To(HaveOccurred()) if tc.expectRequeue { @@ -603,203 +703,1404 @@ var _ = Describe("IPPool manager", func() { } else { Expect(err).NotTo(HaveOccurred()) } + Expect(nbAllocations).To(Equal(tc.expectedNbAllocations)) + Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + // get list of IPAddress objects - addressObjects := ipamv1.IPAddressList{} + addressObjects := ipamv1.IPClaimList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) - Expect(len(tc.expectedIPAddresses)).To(Equal(len(addressObjects.Items))) // Iterate over the IPAddress objects to find all indexes and objects - for _, address := range addressObjects.Items { - Expect(tc.expectedIPAddresses).To(ContainElement(address.Name)) - // TODO add further testing later + for _, claim := range addressObjects.Items { + if claim.DeletionTimestamp.IsZero() { + Expect(claim.Status.Address).NotTo(BeNil()) + } + } + + // get list of IPAddress objects + capiAddressObjects := capipamv1.IPAddressClaimList{} + err = c.List(context.TODO(), &capiAddressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + // Iterate over the IPAddress objects to find all indexes and objects + for _, claim := range capiAddressObjects.Items { + if claim.DeletionTimestamp.IsZero() { + Expect(claim.Status.AddressRef).NotTo(BeNil()) + } } - Expect(len(tc.ipClaim.Finalizers)).To(Equal(1)) - Expect(allocatedMap).To(Equal(tc.expectedAddresses)) - Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) }, - Entry("Already exists", testCaseCreateAddresses{ + Entry("No Claims", testCaseUpdateAddresses{ ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("foo-0"), - }, - }, - }, - ipClaim: &ipamv1.IPClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - }, - }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("foo-0"), }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, }), - Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ + Entry("Claim and IP exist", testCaseUpdateAddresses{ ipPool: &ipamv1.IPPool{ ObjectMeta: ipPoolMeta, Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ - { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), - }, - }, - PreAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.15"), - }, NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, - }, - }, - addresses: map[ipamv1.IPAddressStr]string{}, - ipClaim: &ipamv1.IPClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - }, - }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.15"), - }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.15"): "abc", }, - expectedIPAddresses: []string{"abcpref-192-168-0-15"}, - }), - Entry("Not allocated yet", testCaseCreateAddresses{ - ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ - { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + ipClaims: []*ipamv1.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", }, }, - NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcd", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, }, - }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", - }, - ipClaim: &ipamv1.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abce", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv1.IPClaimFinalizer, + }, + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.11"), + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Prefix: 24, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abce", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.12"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abcf", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.13"), + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.1.11"), + "abce": ipamv1.IPAddressStr("192.168.1.12"), + }, + expectedNbAllocations: 2, + }), + Entry("IPAddressClaim and IP exist", testCaseUpdateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + NamePrefix: "abcpref", + }, + }, + ipAddressClaims: []*capipamv1.IPAddressClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "abcpref-192-168-1-11", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcd", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "abcd", + }, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "abcpref-192-168-1-12", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abce", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "abcpref-192-168-1-12", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + IPAddressClaimFinalizer, + }, + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "abcpref-192-168-1-13", + }, + }, + }, + }, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: *localtestObjectReference, + Address: "192.168.1.11", + Gateway: *ptr.To("192.168.0.1"), + Prefix: 24, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "abce", + }, + Address: "192.168.1.12", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "abcf", + }, + Address: "192.168.1.13", + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "192.168.1.11", + "abce": "192.168.1.12", + }, + expectedNbAllocations: 2, + }), + Entry("Both IPClaim and IPAddressClaim and IP exist", testCaseUpdateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + NamePrefix: "abcpref", + }, + }, + ipClaims: []*ipamv1.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcd", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abce", + Namespace: "myns", + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv1.IPClaimFinalizer, + }, + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv1.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Address: "192.168.1.11", + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Prefix: 24, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abce", + Namespace: "myns", + }, + Address: "192.168.1.12", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abcf", + Namespace: "myns", + }, + Address: "192.168.1.13", + }, + }, + }, + ipAddressClaims: []*capipamv1.IPAddressClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cabc", + }, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "cabcpref-192-168-1-14", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcd", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cabcd", + }, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "cabcpref-192-168-1-15", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabce", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "cabcpref-192-168-1-15", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + IPAddressClaimFinalizer, + }, + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "cabcpref-192-168-1-16", + }, + }, + }, + }, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcpref-192-168-1-14", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "cabc", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "cabc", + }, + Address: "192.168.1.14", + Gateway: *(ptr.To("192.168.0.1")), + Prefix: 24, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcpref-192-168-1-15", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "cabce", + }, + Address: "192.168.1.15", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabcpref-192-168-1-16", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "cabcf", + }, + Address: "192.168.1.16", + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "192.168.1.11", + "abce": "192.168.1.12", + "cabce": "192.168.1.15", + }, + expectedNbAllocations: 3, + }), + Entry("IPClaim with deletion timestamp and finalizers", testCaseUpdateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{}, + }, + ipClaims: []*ipamv1.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "inUseClaim", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv1.IPClaimFinalizer, + "metal3data.infrastructure.cluster.x-k8s.io", + }, + }, + Spec: ipamv1.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "inUseClaim", + Namespace: "myns", + }, + Address: ipamv1.IPAddressStr("192.168.1.11"), + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Prefix: 24, + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "inUseClaim": ipamv1.IPAddressStr("192.168.1.11"), + }, + expectedNbAllocations: 1, + }), + ) + + type testCaseCreateAddresses struct { + ipPool *ipamv1.IPPool + ipClaim *ipamv1.IPClaim + ipAddresses []*ipamv1.IPAddress + addresses map[ipamv1.IPAddressStr]string + expectRequeue bool + expectError bool + expectedIPAddresses []string + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test CreateAddresses", + func(tc testCaseCreateAddresses) { + objects := []client.Object{} + for _, address := range tc.ipAddresses { + objects = append(objects, address) + } + c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build() + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + logr.Discard(), + ) + Expect(err).NotTo(HaveOccurred()) + + allocatedMap, err := ipPoolMgr.createAddress(context.TODO(), tc.ipClaim, + tc.addresses, + ) + if tc.expectRequeue || tc.expectError { + Expect(err).To(HaveOccurred()) + if tc.expectRequeue { + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) + } + } else { + Expect(err).NotTo(HaveOccurred()) + } + // get list of IPAddress objects + addressObjects := ipamv1.IPAddressList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(tc.expectedIPAddresses)).To(Equal(len(addressObjects.Items))) + // Iterate over the IPAddress objects to find all indexes and objects + for _, address := range addressObjects.Items { + Expect(tc.expectedIPAddresses).To(ContainElement(address.Name)) + // TODO add further testing later + } + Expect(len(tc.ipClaim.Finalizers)).To(Equal(1)) + + Expect(allocatedMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + }, + Entry("Already exists", testCaseCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("foo-0"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("foo-0"), + }, + }), + Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.15"), + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.15"), + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.15"): "abc", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-15"}, + }), + Entry("Not allocated yet", testCaseCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.12"), + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "abc", + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-12"}, + }), + Entry("Not allocated yet, conflict", testCaseCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + ipAddresses: []*ipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-0-11", + Namespace: "myns", + }, + Spec: ipamv1.IPAddressSpec{ + Address: "192.168.0.11", + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "bcd", + }, + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedIPAddresses: []string{"abcpref-192-168-0-11"}, + expectRequeue: true, + }), + Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.11"): "bcd", + }, + expectedIPAddresses: []string{}, + expectError: true, + }), + ) + + type testCaseK8sCreateAddresses struct { + ipPool *ipamv1.IPPool + ipAddressClaim *capipamv1.IPAddressClaim + ipAddresses []*capipamv1.IPAddress + addresses map[ipamv1.IPAddressStr]string + expectRequeue bool + expectError bool + expectedIPAddresses []string + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + } + + DescribeTable("Test K8sCreateAddresses", + func(tc testCaseK8sCreateAddresses) { + objects := []client.Object{} + for _, address := range tc.ipAddresses { + objects = append(objects, address) + } + c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build() + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + logr.Discard(), + ) + Expect(err).NotTo(HaveOccurred()) + + allocatedMap, err := ipPoolMgr.K8sCreateAddress(context.TODO(), tc.ipAddressClaim, + tc.addresses, + ) + if tc.expectRequeue || tc.expectError { + Expect(err).To(HaveOccurred()) + if tc.expectRequeue { + Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{})) + } else { + Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{})) + } + } else { + Expect(err).NotTo(HaveOccurred()) + } + // get list of IPAddress objects + addressObjects := capipamv1.IPAddressList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(tc.expectedIPAddresses)).To(Equal(len(addressObjects.Items))) + // Iterate over the IPAddress objects to find all indexes and objects + for _, address := range addressObjects.Items { + Expect(tc.expectedIPAddresses).To(ContainElement(address.Name)) + // TODO add further testing later + } + + Expect(allocatedMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + }, + Entry("Already exists", testCaseK8sCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "abc": "foo-0", + }, + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "foo-0", + }, + }), + Entry("Not allocated yet, pre-allocated", testCaseK8sCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "192.168.0.15", + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": ipamv1.IPAddressStr("192.168.0.15"), + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.15"): "abc", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-15"}, + }), + Entry("Not allocated yet", testCaseK8sCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.11": "bcd", + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{ + "abc": "192.168.0.12", + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.12": "abc", + "192.168.0.11": "bcd", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-12"}, + }), + Entry("Not allocated yet, conflict", testCaseK8sCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + ipAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-0-11", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "192.168.0.11", + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ + Name: "bcd", + }, + }, + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedIPAddresses: []string{"abcpref-192-168-0-11"}, + expectRequeue: true, + }), + Entry("Not allocated yet, exhausted pool", testCaseK8sCreateAddresses{ + ipPool: &ipamv1.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{}, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.11": "bcd", + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.11": "bcd", + }, + expectedIPAddresses: []string{}, + expectError: true, + }), + ) + + type testCaseAllocateAddress struct { + ipPool *ipamv1.IPPool + ipClaim *ipamv1.IPClaim + addresses map[ipamv1.IPAddressStr]string + expectedAddress ipamv1.IPAddressStr + expectedPrefix int + expectedGateway *ipamv1.IPAddressStr + expectedDNSServers []ipamv1.IPAddressStr + expectError bool + } + + DescribeTable("Test AllocateAddress", + func(tc testCaseAllocateAddress) { + ipPoolMgr, err := NewIPPoolManager(nil, tc.ipPool, + logr.Discard(), + ) + Expect(err).NotTo(HaveOccurred()) + allocatedAddress, prefix, gateway, dnsServers, err := ipPoolMgr.allocateAddress( + tc.ipClaim, tc.addresses, + ) + if tc.expectError { + Expect(err).To(HaveOccurred()) + return + } + Expect(err).NotTo(HaveOccurred()) + Expect(allocatedAddress).To(Equal(tc.expectedAddress)) + Expect(prefix).To(Equal(tc.expectedPrefix)) + Expect(*gateway).To(Equal(*tc.expectedGateway)) + Expect(dnsServers).To(Equal(tc.expectedDNSServers)) + }, + Entry("Empty pools", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{}, + }, + ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.12"), + expectError: true, + }), + Entry("One pool, pre-allocated", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.21")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.30")), + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.21"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.12"): "abc", - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.21"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + expectedPrefix: 24, + }), + Entry("One pool, pre-allocated, with overrides", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.15"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + expectedPrefix: 26, + }), + Entry("One pool, pre-allocated, out of bonds", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.21"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectError: true, + }), + Entry("One pool, with start and existing address", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("One pool, with subnet and override prefix", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + expectedPrefix: 24, + }), + Entry("two pools, with subnet and override prefix in first", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + { + Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), + }, + }, + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.1.11"): "bcde", + ipamv1.IPAddressStr("192.168.0.10"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.1.12"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + expectedPrefix: 26, + }), + Entry("two pools, with subnet and override prefix", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + }, + { + Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.1.11"): "bcde", + ipamv1.IPAddressStr("192.168.0.10"): "abcd", }, - expectedIPAddresses: []string{"abcpref-192-168-0-12"}, + expectedAddress: ipamv1.IPAddressStr("192.168.1.12"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + expectedPrefix: 24, }), - Entry("Not allocated yet, conflict", testCaseCreateAddresses{ + Entry("Exhausted pools start", testCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), }, }, - NamePrefix: "abcpref", - }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), }, }, - addresses: map[ipamv1.IPAddressStr]string{}, ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: "abc", + Name: "TestRef", }, }, - ipAddresses: []*ipamv1.IPAddress{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-0-11", - Namespace: "myns", - }, - Spec: ipamv1.IPAddressSpec{ - Address: "192.168.0.11", - Pool: corev1.ObjectReference{ - Name: "abc", - }, - Claim: corev1.ObjectReference{ - Name: "bcd", - }, - }, - }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.10"): "abcd", }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, - expectedIPAddresses: []string{"abcpref-192-168-0-11"}, - expectRequeue: true, + expectError: true, }), - Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ + Entry("Exhausted pools subnet", testCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ - ObjectMeta: ipPoolMeta, Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.0.0/30")), }, }, - NamePrefix: "abcpref", - }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", - }, ipClaim: &ipamv1.IPClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: "abc", + Name: "TestRef", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.1"): "abcd", + ipamv1.IPAddressStr("192.168.0.2"): "abcd", + ipamv1.IPAddressStr("192.168.0.3"): "abcd", }, - expectedIPAddresses: []string{}, - expectError: true, + expectError: true, }), ) - type testCaseAllocateAddress struct { - ipPool *ipamv1.IPPool - ipClaim *ipamv1.IPClaim - addresses map[ipamv1.IPAddressStr]string - expectedAddress ipamv1.IPAddressStr - expectedPrefix int - expectedGateway *ipamv1.IPAddressStr - expectedDNSServers []ipamv1.IPAddressStr - expectError bool + type testK8sCaseAllocateAddress struct { + ipPool *ipamv1.IPPool + addresses map[ipamv1.IPAddressStr]string + ipAddressClaim *capipamv1.IPAddressClaim + expectedAddress ipamv1.IPAddressStr + expectedPrefix int + expectedGateway *ipamv1.IPAddressStr + expectError bool } - DescribeTable("Test AllocateAddress", - func(tc testCaseAllocateAddress) { + DescribeTable("Test K8sAllocateAddress", + func(tc testK8sCaseAllocateAddress) { ipPoolMgr, err := NewIPPoolManager(nil, tc.ipPool, logr.Discard(), ) Expect(err).NotTo(HaveOccurred()) - allocatedAddress, prefix, gateway, dnsServers, err := ipPoolMgr.allocateAddress( - tc.ipClaim, tc.addresses, + allocatedAddress, prefix, gateway, err := ipPoolMgr.K8sAllocateAddress( + tc.ipAddressClaim, tc.addresses, ) if tc.expectError { Expect(err).To(HaveOccurred()) @@ -809,20 +2110,19 @@ var _ = Describe("IPPool manager", func() { Expect(allocatedAddress).To(Equal(tc.expectedAddress)) Expect(prefix).To(Equal(tc.expectedPrefix)) Expect(*gateway).To(Equal(*tc.expectedGateway)) - Expect(dnsServers).To(Equal(tc.expectedDNSServers)) }, - Entry("Empty pools", testCaseAllocateAddress{ + Entry("Empty pools", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{}, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, expectError: true, }), - Entry("One pool, pre-allocated", testCaseAllocateAddress{ + Entry("One pool, pre-allocated", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -831,9 +2131,6 @@ var _ = Describe("IPPool manager", func() { End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, { Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.21")), @@ -845,24 +2142,18 @@ var _ = Describe("IPPool manager", func() { }, Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, expectedAddress: ipamv1.IPAddressStr("192.168.0.21"), expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - expectedDNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, - expectedPrefix: 24, + expectedPrefix: 24, }), - Entry("One pool, pre-allocated, with overrides", testCaseAllocateAddress{ + Entry("One pool, pre-allocated, with overrides", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -871,9 +2162,6 @@ var _ = Describe("IPPool manager", func() { End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, }, PreAllocations: map[string]ipamv1.IPAddressStr{ @@ -881,24 +2169,18 @@ var _ = Describe("IPPool manager", func() { }, Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, expectedAddress: ipamv1.IPAddressStr("192.168.0.15"), expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - expectedDNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, - expectedPrefix: 26, + expectedPrefix: 26, }), - Entry("One pool, pre-allocated, out of bonds", testCaseAllocateAddress{ + Entry("One pool, pre-allocated, out of bonds", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -907,9 +2189,6 @@ var _ = Describe("IPPool manager", func() { End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, }, PreAllocations: map[string]ipamv1.IPAddressStr{ @@ -917,19 +2196,16 @@ var _ = Describe("IPPool manager", func() { }, Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, expectError: true, }), - Entry("One pool, with start and existing address", testCaseAllocateAddress{ + Entry("One pool, with start and existing address", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -942,7 +2218,7 @@ var _ = Describe("IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -955,7 +2231,7 @@ var _ = Describe("IPPool manager", func() { expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), expectedPrefix: 24, }), - Entry("One pool, with subnet and override prefix", testCaseAllocateAddress{ + Entry("One pool, with subnet and override prefix", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -964,19 +2240,13 @@ var _ = Describe("IPPool manager", func() { End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, }, Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -987,12 +2257,9 @@ var _ = Describe("IPPool manager", func() { }, expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - expectedDNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, - expectedPrefix: 24, + expectedPrefix: 24, }), - Entry("two pools, with subnet and override prefix in first", testCaseAllocateAddress{ + Entry("two pools, with subnet and override prefix in first", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -1001,9 +2268,6 @@ var _ = Describe("IPPool manager", func() { End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, { Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), @@ -1011,12 +2275,9 @@ var _ = Describe("IPPool manager", func() { }, Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -1027,12 +2288,9 @@ var _ = Describe("IPPool manager", func() { }, expectedAddress: ipamv1.IPAddressStr("192.168.1.12"), expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), - expectedDNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, - expectedPrefix: 26, + expectedPrefix: 26, }), - Entry("two pools, with subnet and override prefix", testCaseAllocateAddress{ + Entry("two pools, with subnet and override prefix", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -1044,19 +2302,13 @@ var _ = Describe("IPPool manager", func() { Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, }, }, Prefix: 26, Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), - }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -1067,12 +2319,9 @@ var _ = Describe("IPPool manager", func() { }, expectedAddress: ipamv1.IPAddressStr("192.168.1.12"), expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - expectedDNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.8.8"), - }, - expectedPrefix: 24, + expectedPrefix: 24, }), - Entry("Exhausted pools start", testCaseAllocateAddress{ + Entry("Exhausted pools start", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -1085,7 +2334,7 @@ var _ = Describe("IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -1095,7 +2344,7 @@ var _ = Describe("IPPool manager", func() { }, expectError: true, }), - Entry("Exhausted pools subnet", testCaseAllocateAddress{ + Entry("Exhausted pools subnet", testK8sCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ Pools: []ipamv1.Pool{ @@ -1107,7 +2356,7 @@ var _ = Describe("IPPool manager", func() { Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -1236,4 +2485,119 @@ var _ = Describe("IPPool manager", func() { }), ) + type testCaseK8sDeleteAddresses struct { + ipPool *ipamv1.IPPool + ipAddressClaim *capipamv1.IPAddressClaim + capiAddresses []*capipamv1.IPAddress + addresses map[ipamv1.IPAddressStr]string + expectedAddresses map[ipamv1.IPAddressStr]string + expectedAllocations map[string]ipamv1.IPAddressStr + expectError bool + } + + DescribeTable("Test K8sDeleteAddresses", + func(tc testCaseK8sDeleteAddresses) { + objects := []client.Object{} + for _, address := range tc.capiAddresses { + objects = append(objects, address) + } + c := fakeclient.NewClientBuilder().WithScheme(setupScheme()).WithObjects(objects...).Build() + ipPoolMgr, err := NewIPPoolManager(c, tc.ipPool, + logr.Discard(), + ) + Expect(err).NotTo(HaveOccurred()) + + allocatedMap, err := ipPoolMgr.K8sDeleteAddress(context.TODO(), tc.ipAddressClaim, tc.addresses) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + + // get list of IPAddress objects + addressObjects := capipamv1.IPAddressList{} + opts := &client.ListOptions{} + err = c.List(context.TODO(), &addressObjects, opts) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(addressObjects.Items)).To(Equal(0)) + + Expect(tc.ipPool.Status.LastUpdated.IsZero()).To(BeFalse()) + Expect(allocatedMap).To(Equal(tc.expectedAddresses)) + Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) + Expect(len(tc.ipAddressClaim.Finalizers)).To(Equal(0)) + }, + Entry("Empty IPPool", testCaseK8sDeleteAddresses{ + ipPool: &ipamv1.IPPool{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + }), + Entry("No Deletion needed", testCaseK8sDeleteAddresses{ + ipPool: &ipamv1.IPPool{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{"192.168.0.1": "abcd"}, + addresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.1": "abcd", + }, + }), + Entry("Deletion needed, not found", testCaseK8sDeleteAddresses{ + ipPool: &ipamv1.IPPool{ + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "TestRef": "192.168.0.1", + }, + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.1": "TestRef", + }, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + }), + Entry("Deletion needed", testCaseK8sDeleteAddresses{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + NamePrefix: "abc", + }, + Status: ipamv1.IPPoolStatus{ + Allocations: map[string]ipamv1.IPAddressStr{ + "TestRef": "192.168.0.1", + }, + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Finalizers: []string{ + IPAddressClaimFinalizer, + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + "192.168.0.1": "TestRef", + }, + expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]ipamv1.IPAddressStr{}, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-192-168-0-1", + }, + }, + }, + }), + ) + }) diff --git a/ipam/suite_test.go b/ipam/suite_test.go index 925dfac3..5898fe7e 100644 --- a/ipam/suite_test.go +++ b/ipam/suite_test.go @@ -34,6 +34,7 @@ import ( "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/klog/v2" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -71,6 +72,9 @@ var _ = BeforeSuite(func() { err = ipamv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = capipamv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = apiextensionsv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) @@ -103,5 +107,8 @@ func setupScheme() *runtime.Scheme { if err := ipamv1.AddToScheme(s); err != nil { panic(err) } + if err := capipamv1.AddToScheme(s); err != nil { + panic(err) + } return s } diff --git a/main.go b/main.go index 9c5bd327..9ba0ac18 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ import ( logsv1 "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util/flags" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -68,6 +69,7 @@ func init() { _ = scheme.AddToScheme(myscheme) _ = ipamv1.AddToScheme(myscheme) _ = clusterv1.AddToScheme(myscheme) + _ = capipamv1.AddToScheme(myscheme) } // Add RBAC for the authorized diagnostics endpoint. @@ -216,8 +218,18 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager) { ManagerFactory: ipam.NewManagerFactory(mgr.GetClient()), Log: ctrl.Log.WithName("controllers").WithName("IPPool"), WatchFilterValue: watchFilterValue, - }).SetupWithManager(ctx, mgr, concurrency(ippoolConcurrency)); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "IPPoolReconciler") + }).SetupWithManagerForIPClaim(ctx, mgr, concurrency(ippoolConcurrency)); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IPPoolReconciler for IPClaims") + os.Exit(1) + } + + if err := (&controllers.IPPoolReconciler{ + Client: mgr.GetClient(), + ManagerFactory: ipam.NewManagerFactory(mgr.GetClient()), + Log: ctrl.Log.WithName("controllers").WithName("IPPool"), + WatchFilterValue: watchFilterValue, + }).SetupWithManagerForIPAddressClaim(ctx, mgr, concurrency(ippoolConcurrency)); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IPPoolReconciler for IPAddressClaim") os.Exit(1) } }