From 4fcd34f549487edd35363bc0383d79121a9d99a7 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 --- .golangci.yaml | 4 +- Dockerfile | 2 +- README.md | 2 +- api/v1alpha1/doc.go | 1 + api/v1alpha1/ipaddress_types.go | 1 - api/v1alpha1/ipclaim_types.go | 1 - api/v1alpha1/ippool_types.go | 1 - api/v1alpha2/conversion.go | 19 + api/v1alpha2/doc.go | 23 + api/v1alpha2/groupversion_info.go | 53 + api/v1alpha2/ipaddress_types.go | 77 + api/v1alpha2/ipaddress_webhook.go | 159 ++ api/v1alpha2/ipaddress_webhook_test.go | 320 +++ api/v1alpha2/ipclaim_types.go | 73 + api/v1alpha2/ipclaim_webhook.go | 103 + api/v1alpha2/ipclaim_webhook_test.go | 220 ++ api/v1alpha2/ippool_types.go | 120 + api/v1alpha2/ippool_webhook.go | 176 ++ api/v1alpha2/ippool_webhook_test.go | 246 ++ api/v1alpha2/suite_test.go | 42 + api/v1alpha2/utils.go | 124 + api/v1alpha2/utils_test.go | 189 ++ api/v1alpha2/zz_generated.deepcopy.go | 380 +++ .../crd/bases/ipam.metal3.io_ipaddresses.yaml | 123 + config/crd/bases/ipam.metal3.io_ipclaims.yaml | 115 + config/crd/bases/ipam.metal3.io_ippools.yaml | 111 + config/default/kustomization.yaml | 7 +- config/manager/manager.yaml | 7 + config/rbac/role.yaml | 40 + config/webhook/manifests.yaml | 132 + controllers/ippool_controller.go | 81 +- controllers/ippool_controller_test.go | 114 +- controllers/suite_test.go | 18 +- examples/generate.sh | 2 +- examples/ippool/ippool.yaml | 14 +- examples/ippool/kustomizeconfig.yaml | 2 +- .../manager_tolerations_patch.yaml | 2 +- ipam/ippool_manager.go | 489 +++- ipam/ippool_manager_test.go | 2162 ++++++++++++++--- ipam/manager_factory.go | 6 +- ipam/manager_factory_test.go | 4 +- ipam/mocks/zz_generated.manager_factory.go | 4 +- ipam/suite_test.go | 14 +- ipam/utils_test.go | 48 +- main.go | 26 +- test/fuzz/ippool_controller_fuzzer.go | 4 +- test/fuzz/ippool_manager_fuzzer.go | 4 +- 47 files changed, 5333 insertions(+), 532 deletions(-) create mode 100644 api/v1alpha2/conversion.go create mode 100644 api/v1alpha2/doc.go create mode 100644 api/v1alpha2/groupversion_info.go create mode 100644 api/v1alpha2/ipaddress_types.go create mode 100644 api/v1alpha2/ipaddress_webhook.go create mode 100644 api/v1alpha2/ipaddress_webhook_test.go create mode 100644 api/v1alpha2/ipclaim_types.go create mode 100644 api/v1alpha2/ipclaim_webhook.go create mode 100644 api/v1alpha2/ipclaim_webhook_test.go create mode 100644 api/v1alpha2/ippool_types.go create mode 100644 api/v1alpha2/ippool_webhook.go create mode 100644 api/v1alpha2/ippool_webhook_test.go create mode 100644 api/v1alpha2/suite_test.go create mode 100644 api/v1alpha2/utils.go create mode 100644 api/v1alpha2/utils_test.go create mode 100644 api/v1alpha2/zz_generated.deepcopy.go diff --git a/.golangci.yaml b/.golangci.yaml index 3282b469..39bc4779 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -69,8 +69,8 @@ linters-settings: - pkg: sigs.k8s.io/cluster-api/api/v1beta1 alias: clusterv1 # IPAM - - pkg: github.com/metal3-io/ip-address-manager/api/v1alpha1 - alias: ipamv1 + - pkg: github.com/metal3-io/ip-address-manager/api/v1alpha2 + alias: ipamv2 nolintlint: allow-unused: false allow-leading-space: false 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/api/v1alpha1/doc.go b/api/v1alpha1/doc.go index ca3b1471..70e320d0 100644 --- a/api/v1alpha1/doc.go +++ b/api/v1alpha1/doc.go @@ -20,4 +20,5 @@ limitations under the License. // +k8s:defaulter-gen=TypeMeta // +kubebuilder:object:generate=true // +groupName=ipam.metal3.io +// +k8s:conversion-gen=github.com/metal3-io/ip-address-manager/api/v1alpha2 package v1alpha1 diff --git a/api/v1alpha1/ipaddress_types.go b/api/v1alpha1/ipaddress_types.go index d2b0be4f..63175eed 100644 --- a/api/v1alpha1/ipaddress_types.go +++ b/api/v1alpha1/ipaddress_types.go @@ -53,7 +53,6 @@ type IPAddressSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:path=ipaddresses,scope=Namespaced,categories=metal3,shortName=ipa;ipaddress;m3ipa;m3ipaddress;m3ipaddresses;metal3ipa;metal3ipaddress;metal3ipaddresses // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Metal3IPAddress" -// +kubebuilder:storageversion // +kubebuilder:object:root=true // IPAddress is the Schema for the ipaddresses API. type IPAddress struct { diff --git a/api/v1alpha1/ipclaim_types.go b/api/v1alpha1/ipclaim_types.go index 93810896..4d76bacc 100644 --- a/api/v1alpha1/ipclaim_types.go +++ b/api/v1alpha1/ipclaim_types.go @@ -46,7 +46,6 @@ type IPClaimStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:path=ipclaims,scope=Namespaced,categories=cluster-api,shortName=ipc;ipclaim;m3ipc;m3ipclaim;m3ipclaims;metal3ipc;metal3ipclaim;metal3ipclaims -// +kubebuilder:storageversion // +kubebuilder:subresource:status // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Metal3IPClaim" diff --git a/api/v1alpha1/ippool_types.go b/api/v1alpha1/ippool_types.go index 4b41dd53..e2ddddf7 100644 --- a/api/v1alpha1/ippool_types.go +++ b/api/v1alpha1/ippool_types.go @@ -92,7 +92,6 @@ type IPPoolStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:path=ippools,scope=Namespaced,categories=cluster-api,shortName=ipp;ippool;m3ipp;m3ippool;m3ippools;metal3ipp;metal3ippool;metal3ippools -// +kubebuilder:storageversion // +kubebuilder:subresource:status // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this template belongs" diff --git a/api/v1alpha2/conversion.go b/api/v1alpha2/conversion.go new file mode 100644 index 00000000..1e589bcd --- /dev/null +++ b/api/v1alpha2/conversion.go @@ -0,0 +1,19 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +func (*IPPool) Hub() {} diff --git a/api/v1alpha2/doc.go b/api/v1alpha2/doc.go new file mode 100644 index 00000000..7d391aaf --- /dev/null +++ b/api/v1alpha2/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha2 contains API Schema definitions for the metal3 v1alpha2 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:defaulter-gen=TypeMeta +// +kubebuilder:object:generate=true +// +groupName=ipam.metal3.io +package v1alpha2 diff --git a/api/v1alpha2/groupversion_info.go b/api/v1alpha2/groupversion_info.go new file mode 100644 index 00000000..bf8f7cea --- /dev/null +++ b/api/v1alpha2/groupversion_info.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha2 contains API Schema definitions for the infrastructure v1alpha2 API group +// +kubebuilder:object:generate=true +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:defaulter-gen=TypeMeta +// +groupName=ipam.metal3.io +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "ipam.metal3.io", Version: "v1alpha2"} + + /// schemeBuilder is used to add go types to the GroupVersionKind scheme. + schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = schemeBuilder.AddToScheme + + objectTypes = []runtime.Object{} +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, objectTypes...) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} + +// Resource is required by pkg/client/listers/... +// func Resource(resource string) schema.GroupResource { +// return SchemeGroupVersion.WithResource(resource).GroupResource() +// } diff --git a/api/v1alpha2/ipaddress_types.go b/api/v1alpha2/ipaddress_types.go new file mode 100644 index 00000000..0d3008b2 --- /dev/null +++ b/api/v1alpha2/ipaddress_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // DataFinalizer allows IPAddressReconciler to clean up resources + // associated with IPAddress before removing it from the apiserver. + IPAddressFinalizer = "ipaddress.ipam.metal3.io" +) + +// IPAddressSpec defines the desired state of IPAddress. +type IPAddressSpec struct { + + // Claim points to the object the IPClaim was created for. + Claim corev1.ObjectReference `json:"claim"` + + // Pool is the IPPool this was generated from. + Pool corev1.ObjectReference `json:"pool"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *string `json:"gateway,omitempty"` + + // Address contains the IP address + Address string `json:"address"` + + // DNSServers is the list of dns servers + DNSServers []string `json:"dnsServers,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=ipaddresses,scope=Namespaced,categories=metal3,shortName=ipa;ipaddress;m3ipa;m3ipaddress;m3ipaddresses;metal3ipa;metal3ipaddress;metal3ipaddresses +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Metal3IPAddress" +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// IPAddress is the Schema for the ipaddresses API. +type IPAddress struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec IPAddressSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// IPAddressList contains a list of IPAddress. +type IPAddressList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IPAddress `json:"items"` +} + +func init() { + objectTypes = append(objectTypes, &IPAddress{}, &IPAddressList{}) +} diff --git a/api/v1alpha2/ipaddress_webhook.go b/api/v1alpha2/ipaddress_webhook.go new file mode 100644 index 00000000..0d1e936c --- /dev/null +++ b/api/v1alpha2/ipaddress_webhook.go @@ -0,0 +1,159 @@ +/* +Copyright 2020 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func (c *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha2-ipaddress,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha2,name=validation.ipaddress.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha2-ipaddress,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipaddresses,versions=v1alpha2,name=default.ipaddress.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +var _ webhook.Defaulter = &IPAddress{} +var _ webhook.Validator = &IPAddress{} + +func (c *IPAddress) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPAddress) ValidateCreate() (admission.Warnings, error) { + allErrs := field.ErrorList{} + if c.Spec.Pool.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool", "name"), + c.Spec.Pool.Name, + "cannot be empty", + ), + ) + } + + if c.Spec.Claim.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim", "name"), + c.Spec.Claim.Name, + "cannot be empty", + ), + ) + } + + if c.Spec.Address == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "address"), + c.Spec.Address, + "cannot be empty", + ), + ) + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("IPAddress").GroupKind(), c.Name, allErrs) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPAddress) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + allErrs := field.ErrorList{} + oldIPAddress, ok := old.(*IPAddress) + if !ok || oldIPAddress == nil { + return nil, apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if c.Spec.Address != oldIPAddress.Spec.Address { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "address"), + c.Spec.Address, + "cannot be modified", + ), + ) + } + + if c.Spec.Pool.Name != oldIPAddress.Spec.Pool.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Namespace != oldIPAddress.Spec.Pool.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Kind != oldIPAddress.Spec.Pool.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } + + if c.Spec.Claim.Name != oldIPAddress.Spec.Claim.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } else if c.Spec.Claim.Namespace != oldIPAddress.Spec.Claim.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } else if c.Spec.Claim.Kind != oldIPAddress.Spec.Claim.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "claim"), + c.Spec.Claim, + "cannot be modified", + ), + ) + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("IPAddress").GroupKind(), c.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (c *IPAddress) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} diff --git a/api/v1alpha2/ipaddress_webhook_test.go b/api/v1alpha2/ipaddress_webhook_test.go new file mode 100644 index 00000000..b3db86e2 --- /dev/null +++ b/api/v1alpha2/ipaddress_webhook_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2019 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIPAddressDefault(t *testing.T) { + g := NewWithT(t) + + c := &IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: IPAddressSpec{}, + } + c.Default() + + g.Expect(c.Spec).To(Equal(IPAddressSpec{})) +} + +func TestIPAddressCreateValidation(t *testing.T) { + tests := []struct { + name string + addressName string + expectErr bool + ipPool corev1.ObjectReference + address string + claim corev1.ObjectReference + }{ + { + name: "should succeed when values and ipPools correct", + expectErr: false, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + address: "abcd", + }, + { + name: "should fail without address", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + }, + { + name: "should fail without ipPool name", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Namespace: "abc", + }, + claim: corev1.ObjectReference{ + Name: "abc", + }, + address: "abcd", + }, + { + name: "should fail without claim name", + expectErr: true, + addressName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + claim: corev1.ObjectReference{ + Namespace: "abc", + }, + address: "abcd", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + obj := &IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: tt.addressName, + }, + Spec: IPAddressSpec{ + Pool: tt.ipPool, + Address: tt.address, + Claim: tt.claim, + }, + } + + if tt.expectErr { + _, err := obj.ValidateCreate() + g.Expect(err).To(HaveOccurred()) + } else { + _, err := obj.ValidateCreate() + g.Expect(err).NotTo(HaveOccurred()) + } + _, err := obj.ValidateDelete() + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func TestIPAddressUpdateValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + newAdd *IPAddressSpec + old *IPAddressSpec + }{ + { + name: "should succeed when values are the same", + expectErr: false, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + }, + { + name: "should fail with nil old", + expectErr: true, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: nil, + }, + { + name: "should fail when index changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcde", + }, + }, + { + name: "should fail when pool name changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Pool Namespace changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Pool kind changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim name changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim Namespace changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + Address: "abcd", + }, + }, + { + name: "should fail when Claim kind changes", + expectErr: true, + newAdd: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + Address: "abcd", + }, + old: &IPAddressSpec{ + Claim: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + Address: "abcd", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var newAdd, old *IPAddress + g := NewWithT(t) + newAdd = &IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.newAdd, + } + + if tt.old != nil { + old = &IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.old, + } + } else { + old = nil + } + + if tt.expectErr { + _, err := newAdd.ValidateUpdate(old) + g.Expect(err).To(HaveOccurred()) + } else { + _, err := newAdd.ValidateUpdate(old) + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/api/v1alpha2/ipclaim_types.go b/api/v1alpha2/ipclaim_types.go new file mode 100644 index 00000000..7a8ee9fe --- /dev/null +++ b/api/v1alpha2/ipclaim_types.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // IPClaimFinalizer allows IPClaimReconciler to clean up resources + // associated with IPClaim before removing it from the apiserver. + IPClaimFinalizer = "ipclaim.ipam.metal3.io" +) + +// IPClaimSpec defines the desired state of IPClaim. +type IPClaimSpec struct { + + // Pool is the IPPool this was generated from. + Pool corev1.ObjectReference `json:"pool"` +} + +// IPClaimStatus defines the observed state of IPClaim. +type IPClaimStatus struct { + + // Address is the IPAddress that was generated for this claim. + Address *corev1.ObjectReference `json:"address,omitempty"` + + // ErrorMessage contains the error message + ErrorMessage *string `json:"errorMessage,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=ipclaims,scope=Namespaced,categories=cluster-api,shortName=ipc;ipclaim;m3ipc;m3ipclaim;m3ipclaims;metal3ipc;metal3ipclaim;metal3ipclaims +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Metal3IPClaim" +// IPClaim is the Schema for the ipclaims API. +type IPClaim struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec IPClaimSpec `json:"spec,omitempty"` + Status IPClaimStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// IPClaimList contains a list of IPClaim. +type IPClaimList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IPClaim `json:"items"` +} + +func init() { + objectTypes = append(objectTypes, &IPClaim{}, &IPClaimList{}) +} diff --git a/api/v1alpha2/ipclaim_webhook.go b/api/v1alpha2/ipclaim_webhook.go new file mode 100644 index 00000000..9820aec7 --- /dev/null +++ b/api/v1alpha2/ipclaim_webhook.go @@ -0,0 +1,103 @@ +/* +Copyright 2020 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func (c *IPClaim) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha2-ipclaim,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha2,name=validation.ipclaim.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha2-ipclaim,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ipclaims,versions=v1alpha2,name=default.ipclaim.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +var _ webhook.Defaulter = &IPClaim{} +var _ webhook.Validator = &IPClaim{} + +func (c *IPClaim) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPClaim) ValidateCreate() (admission.Warnings, error) { + allErrs := field.ErrorList{} + if c.Spec.Pool.Name == "" { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool", "name"), + c.Spec.Pool.Name, + "cannot be empty", + ), + ) + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("IPClaim").GroupKind(), c.Name, allErrs) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPClaim) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + allErrs := field.ErrorList{} + oldIPClaim, ok := old.(*IPClaim) + if !ok || oldIPClaim == nil { + return nil, apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if c.Spec.Pool.Name != oldIPClaim.Spec.Pool.Name { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Namespace != oldIPClaim.Spec.Pool.Namespace { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } else if c.Spec.Pool.Kind != oldIPClaim.Spec.Pool.Kind { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pool"), + c.Spec.Pool, + "cannot be modified", + ), + ) + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("IPClaim").GroupKind(), c.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (c *IPClaim) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} diff --git a/api/v1alpha2/ipclaim_webhook_test.go b/api/v1alpha2/ipclaim_webhook_test.go new file mode 100644 index 00000000..4d5c665e --- /dev/null +++ b/api/v1alpha2/ipclaim_webhook_test.go @@ -0,0 +1,220 @@ +/* +Copyright 2019 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIPClaimDefault(t *testing.T) { + g := NewWithT(t) + + c := &IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + } + c.Default() + + g.Expect(c.Spec).To(Equal(IPClaimSpec{})) + g.Expect(c.Status).To(Equal(IPClaimStatus{})) +} + +func TestIPClaimCreateValidation(t *testing.T) { + tests := []struct { + name string + claimName string + expectErr bool + ipPool corev1.ObjectReference + }{ + { + name: "should succeed when ipPool is correct", + expectErr: false, + claimName: "abc-1", + ipPool: corev1.ObjectReference{ + Name: "abc", + }, + }, + { + name: "should fail without ipPool", + expectErr: true, + claimName: "abc-1", + ipPool: corev1.ObjectReference{}, + }, + { + name: "should fail without ipPool name", + expectErr: true, + claimName: "abc-1", + ipPool: corev1.ObjectReference{ + Namespace: "abc", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + obj := &IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: tt.claimName, + }, + Spec: IPClaimSpec{ + Pool: tt.ipPool, + }, + } + + if tt.expectErr { + _, err := obj.ValidateCreate() + g.Expect(err).To(HaveOccurred()) + } else { + _, err := obj.ValidateCreate() + g.Expect(err).NotTo(HaveOccurred()) + } + _, err := obj.ValidateDelete() + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func TestIPClaimUpdateValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + newClm *IPClaimSpec + old *IPClaimSpec + }{ + { + name: "should succeed when values are the same", + expectErr: false, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + }, + { + name: "should fail with nil old", + expectErr: true, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: nil, + }, + { + name: "should fail when pool is unset", + expectErr: true, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{}, + }, + old: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + }, + { + name: "should fail when pool name changes", + expectErr: true, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + }, + }, + old: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + }, + }, + }, + { + name: "should fail when Pool Namespace changes", + expectErr: true, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abc", + }, + }, + old: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "abcd", + }, + }, + }, + { + name: "should fail when Pool kind changes", + expectErr: true, + newClm: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abc", + }, + }, + old: &IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Kind: "abcd", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var newClm, old *IPClaim + g := NewWithT(t) + newClm = &IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.newClm, + } + + if tt.old != nil { + old = &IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "abc-1", + }, + Spec: *tt.old, + } + } else { + old = nil + } + + if tt.expectErr { + _, err := newClm.ValidateUpdate(old) + g.Expect(err).To(HaveOccurred()) + } else { + _, err := newClm.ValidateUpdate(old) + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/api/v1alpha2/ippool_types.go b/api/v1alpha2/ippool_types.go new file mode 100644 index 00000000..e3d8d8fa --- /dev/null +++ b/api/v1alpha2/ippool_types.go @@ -0,0 +1,120 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // IPPoolFinalizer allows IPPoolReconciler to clean up resources + // associated with IPPool before removing it from the apiserver. + IPPoolFinalizer = "ippool.ipam.metal3.io" +) + +// MetaDataIPAddress contains the info to render th ip address. It is IP-version +// agnostic. +type Pool struct { + + // Start is the first ip address that can be rendered + Start *string `json:"start,omitempty"` + + // End is the last IP address that can be rendered. It is used as a validation + // that the rendered IP is in bound. + End *string `json:"end,omitempty"` + + // Subnet is used to validate that the rendered IP is in bounds. In case the + // Start value is not given, it is derived from the subnet ip incremented by 1 + // (`192.168.0.1` for `192.168.0.0/24`) + Subnet *string `json:"subnet,omitempty"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *string `json:"gateway,omitempty"` + + // DNSServers is the list of dns servers + DNSServers []string `json:"dnsServers,omitempty"` +} + +// IPPoolSpec defines the desired state of IPPool. +type IPPoolSpec struct { + + // ClusterName is the name of the Cluster this object belongs to. + ClusterName *string `json:"clusterName,omitempty"` + + // Pools contains the list of IP addresses pools + Pools []Pool `json:"pools,omitempty"` + + // PreAllocations contains the preallocated IP addresses + PreAllocations map[string]string `json:"preAllocations,omitempty"` + + // +kubebuilder:validation:Maximum=128 + // Prefix is the mask of the network as integer (max 128) + Prefix int `json:"prefix,omitempty"` + + // Gateway is the gateway ip address + Gateway *string `json:"gateway,omitempty"` + + // DNSServers is the list of dns servers + DNSServers []string `json:"dnsServers,omitempty"` + + // +kubebuilder:validation:MinLength=1 + // namePrefix is the prefix used to generate the IPAddress object names + NamePrefix string `json:"namePrefix"` +} + +// IPPoolStatus defines the observed state of IPPool. +type IPPoolStatus struct { + // LastUpdated identifies when this status was last observed. + // +optional + LastUpdated *metav1.Time `json:"lastUpdated,omitempty"` + + // Allocations contains the map of objects and IP addresses they have + Allocations map[string]string `json:"indexes,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=ippools,scope=Namespaced,categories=cluster-api,shortName=ipp;ippool;m3ipp;m3ippool;m3ippools;metal3ipp;metal3ippool;metal3ippools +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this template belongs" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Metal3IPPool" +// IPPool is the Schema for the ippools API. +type IPPool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec IPPoolSpec `json:"spec,omitempty"` + Status IPPoolStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// IPPoolList contains a list of IPPool. +type IPPoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IPPool `json:"items"` +} + +func init() { + objectTypes = append(objectTypes, &IPPool{}, &IPPoolList{}) +} diff --git a/api/v1alpha2/ippool_webhook.go b/api/v1alpha2/ippool_webhook.go new file mode 100644 index 00000000..7331a1f5 --- /dev/null +++ b/api/v1alpha2/ippool_webhook.go @@ -0,0 +1,176 @@ +/* +Copyright 2020 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "net" + "net/netip" + "reflect" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func (c *IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(c). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-ipam-metal3-io-v1alpha2-ippool,mutating=false,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha2,name=validation.ippool.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-ipam-metal3-io-v1alpha2-ippool,mutating=true,failurePolicy=fail,groups=ipam.metal3.io,resources=ippools,versions=v1alpha2,name=default.ippool.ipam.metal3.io,matchPolicy=Equivalent,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +var _ webhook.Defaulter = &IPPool{} +var _ webhook.Validator = &IPPool{} + +func (c *IPPool) Default() { +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPPool) ValidateCreate() (admission.Warnings, error) { + return c.validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (c *IPPool) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + allErrs := field.ErrorList{} + oldM3ipp, ok := old.(*IPPool) + if !ok || oldM3ipp == nil { + return nil, apierrors.NewInternalError(errors.New("unable to convert existing object")) + } + + if !reflect.DeepEqual(c.Spec.NamePrefix, oldM3ipp.Spec.NamePrefix) { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "NamePrefix"), + c.Spec.NamePrefix, + "cannot be modified", + ), + ) + } + allocationOutOfBonds, inUseOutOfBonds := c.checkPoolBonds(oldM3ipp) + if len(allocationOutOfBonds) != 0 { + for _, address := range allocationOutOfBonds { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "preAllocations"), + address, + "is out of bonds of the pools given", + ), + ) + } + } + if len(inUseOutOfBonds) != 0 { + for _, address := range inUseOutOfBonds { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pools"), + address, + "is in use but out of bonds of the pools given", + ), + ) + } + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("Metal3Data").GroupKind(), c.Name, allErrs) +} + +func (c *IPPool) checkPoolBonds(old *IPPool) ([]string, []string) { + allocationOutOfBonds := []string{} + inUseOutOfBonds := []string{} + for _, address := range c.Spec.PreAllocations { + inBonds := c.isAddressInBonds(address) + + if !inBonds { + allocationOutOfBonds = append(allocationOutOfBonds, address) + } + } + for _, address := range old.Status.Allocations { + inBonds := c.isAddressInBonds(address) + + if !inBonds { + inUseOutOfBonds = append(inUseOutOfBonds, address) + } + } + return allocationOutOfBonds, inUseOutOfBonds +} + +func (c *IPPool) isAddressInBonds(address string) bool { + ip, err := netip.ParseAddr(address) + if err != nil { + return false + } + + for _, pool := range c.Spec.Pools { + if pool.Start != nil { + startIP, err := netip.ParseAddr(*pool.Start) + if err != nil { + // skip this invalid pool, as the validation error should be caught somewhere else + continue + } + if startIP.Compare(ip) > 0 { + continue + } + } + + if pool.End != nil { + endIP, err := netip.ParseAddr(*pool.End) + if err != nil { + // skip this invalid pool, as the validation error should be caught somewhere else + continue + } + if endIP.Compare(ip) < 0 { + continue + } + } + + if pool.Subnet != nil { + _, subnet, err := net.ParseCIDR(*pool.Subnet) + if err != nil { + // skip this invalid pool, as the validation error should be caught somewhere else + continue + } + if !subnet.Contains(net.ParseIP(ip.String())) { + continue + } + } + + return true + } + + return false +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (c *IPPool) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +// No further validation for now. +func (c *IPPool) validate() (admission.Warnings, error) { + var allErrs field.ErrorList + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid(GroupVersion.WithKind("IPPool").GroupKind(), c.Name, allErrs) +} diff --git a/api/v1alpha2/ippool_webhook_test.go b/api/v1alpha2/ippool_webhook_test.go new file mode 100644 index 00000000..c9ee8004 --- /dev/null +++ b/api/v1alpha2/ippool_webhook_test.go @@ -0,0 +1,246 @@ +/* +Copyright 2019 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIPPoolDefault(t *testing.T) { + g := NewWithT(t) + + c := &IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: IPPoolSpec{}, + } + c.Default() + + g.Expect(c.Spec).To(Equal(IPPoolSpec{})) + g.Expect(c.Status).To(Equal(IPPoolStatus{})) +} + +func TestIPPoolValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + c *IPPool + }{ + { + name: "should succeed when values and templates correct", + expectErr: false, + c: &IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: IPPoolSpec{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + if tt.expectErr { + _, err := tt.c.ValidateCreate() + g.Expect(err).To(HaveOccurred()) + } else { + _, err := tt.c.ValidateCreate() + g.Expect(err).NotTo(HaveOccurred()) + } + _, err := tt.c.ValidateDelete() + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func TestIPPoolUpdateValidation(t *testing.T) { + startAddr := "192.168.0.1" + endAddr := "192.168.0.10" + subnet := "192.168.0.1/25" + + tests := []struct { + name string + expectErr bool + newPoolSpec *IPPoolSpec + oldPoolSpec *IPPoolSpec + oldPoolStatus IPPoolStatus + }{ + { + name: "should succeed when values and templates correct", + expectErr: false, + newPoolSpec: &IPPoolSpec{}, + oldPoolSpec: &IPPoolSpec{}, + }, + { + name: "should fail when oldPoolSpec is nil", + expectErr: true, + newPoolSpec: &IPPoolSpec{}, + oldPoolSpec: nil, + }, + { + name: "should fail when namePrefix value changes", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcde", + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + }, + { + name: "should succeed when preAllocations are between start and end", + expectErr: false, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]string{ + "alloc": "192.168.0.2", + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]string{ + "inuse": "192.168.0.3", + }, + }, + }, + { + name: "should fail when preAllocations are out of start and end", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]string{ + "alloc": "192.168.0.20", + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]string{ + "inuse": "192.168.0.3", + }, + }, + }, + { + name: "should succeed when preAllocations are in the cidr", + expectErr: false, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Subnet: &subnet}, + }, + PreAllocations: map[string]string{ + "alloc": "192.168.0.2", + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]string{ + "inuse": "192.168.0.3", + }, + }, + }, + { + name: "should fail when preAllocations are out of cidr", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Subnet: &subnet}, + }, + PreAllocations: map[string]string{ + "alloc": "192.168.0.250", + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]string{ + "inuse": "192.168.0.3", + }, + }, + }, + { + name: "should fail when ip in use", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]string{ + "alloc": "192.168.0.2", + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]string{ + "inuse": "192.168.0.30", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var newPool, oldPool *IPPool + g := NewWithT(t) + newPool = &IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: *tt.newPoolSpec, + } + + if tt.oldPoolSpec != nil { + oldPool = &IPPool{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: *tt.oldPoolSpec, + Status: tt.oldPoolStatus, + } + } else { + oldPool = nil + } + + if tt.expectErr { + _, err := newPool.ValidateUpdate(oldPool) + g.Expect(err).To(HaveOccurred()) + } else { + _, err := newPool.ValidateUpdate(oldPool) + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/api/v1alpha2/suite_test.go b/api/v1alpha2/suite_test.go new file mode 100644 index 00000000..3040f990 --- /dev/null +++ b/api/v1alpha2/suite_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/klog/v2" + + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + // +kubebuilder:scaffold:imports +) + +func init() { + klog.InitFlags(nil) + klog.SetOutput(GinkgoWriter) + ctrl.SetLogger(klog.Background()) + logf.SetLogger(klog.Background()) +} + +func TestAPI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "API Suite") +} diff --git a/api/v1alpha2/utils.go b/api/v1alpha2/utils.go new file mode 100644 index 00000000..9a433be7 --- /dev/null +++ b/api/v1alpha2/utils.go @@ -0,0 +1,124 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "fmt" + "math/big" + "net" + + "github.com/pkg/errors" +) + +// GetIPAddress renders the IP address, taking the index, offset and step into +// account, it is IP version agnostic. +func GetIPAddress(entry Pool, index int) (string, error) { + if entry.Start == nil && entry.Subnet == nil { + return "", errors.New("Either Start or Subnet is required for ipAddress") + } + var ip net.IP + var err error + var ipNet *net.IPNet + offset := index + + // If start is given, use it to add the offset. + if entry.Start != nil { + var endIP net.IP + if entry.End != nil { + endIP = net.ParseIP(*entry.End) + } + ip, err = addOffsetToIP(net.ParseIP(*entry.Start), endIP, offset) + if err != nil { + return "", err + } + + // Verify that the IP is in the subnet. + if entry.Subnet != nil { + _, ipNet, err = net.ParseCIDR(*entry.Subnet) + if err != nil { + return "", err + } + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + + // If it is not given, use the CIDR ip address and increment the offset by 1. + } else { + ip, ipNet, err = net.ParseCIDR(*entry.Subnet) + if err != nil { + return "", err + } + offset++ + ip, err = addOffsetToIP(ip, nil, offset) + if err != nil { + return "", err + } + + // Verify that the ip is in the subnet. + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + return ip.String(), nil +} + +// addOffsetToIP computes the value of the IP address with the offset. It is +// IP version agnostic +// Note that if the resulting IP address is in the format ::ffff:xxxx:xxxx then +// ip.String will fail to select the correct type of ip. +func addOffsetToIP(ip, endIP net.IP, offset int) (net.IP, error) { + ip4 := false + if ip.To4() != nil { + ip4 = true + } + + // Create big integers. + IPInt := big.NewInt(0) + OffsetInt := big.NewInt(int64(offset)) + + // Transform the ip into an int. (big endian function). + IPInt = IPInt.SetBytes(ip) + + // add the two integers. + IPInt = IPInt.Add(IPInt, OffsetInt) + + // return the bytes list. + IPBytes := IPInt.Bytes() + + IPBytesLen := len(IPBytes) + + // Verify that the IPv4 or IPv6 fulfills theirs constraints. + if (ip4 && IPBytesLen > 6 && IPBytes[4] != 255 && IPBytes[5] != 255) || + (!ip4 && IPBytesLen > 16) { + return nil, fmt.Errorf("IP address overflow for : %s", ip.String()) + } + + // transform the end ip into an Int to compare. + if endIP != nil { + endIPInt := big.NewInt(0) + endIPInt = endIPInt.SetBytes(endIP) + // Computed IP is higher than the end IP. + if IPInt.Cmp(endIPInt) > 0 { + return nil, fmt.Errorf("IP address out of bonds for : %s", ip.String()) + } + } + + // COpy the output back into an ip. + copy(ip[16-IPBytesLen:], IPBytes) + return ip, nil +} diff --git a/api/v1alpha2/utils_test.go b/api/v1alpha2/utils_test.go new file mode 100644 index 00000000..779f1c94 --- /dev/null +++ b/api/v1alpha2/utils_test.go @@ -0,0 +1,189 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "net" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/utils/ptr" +) + +var _ = Describe("IPPool manager", func() { + type testCaseGetIPAddress struct { + ipAddress Pool + index int + expectError bool + expectedIP string + } + + DescribeTable("Test getIPAddress", + func(tc testCaseGetIPAddress) { + result, err := GetIPAddress(tc.ipAddress, tc.index) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(tc.expectedIP)) + } + }, + Entry("Empty Start and Subnet", testCaseGetIPAddress{ + ipAddress: Pool{}, + index: 1, + expectError: true, + }), + Entry("Start set, no end or subnet", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + }, + index: 1, + expectedIP: "192.168.0.11", + }), + Entry("Start set, end set, subnet unset", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.100"), + }, + index: 1, + expectedIP: "192.168.0.11", + }), + Entry("Start set, end set, subnet unset, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.100"), + }, + index: 100, + expectError: true, + }), + Entry("Start set, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + Subnet: ptr.To("192.168.0.0/24"), + }, + index: 1, + expectedIP: "192.168.0.11", + }), + Entry("Start set, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + Subnet: ptr.To("192.168.0.0/24"), + }, + index: 250, + expectError: true, + }), + Entry("Start set, end unset, subnet empty", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: ptr.To("192.168.0.10"), + Subnet: ptr.To(""), + }, + index: 1, + expectError: true, + }), + Entry("subnet empty", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: ptr.To(""), + }, + index: 1, + expectError: true, + }), + Entry("Start unset, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: ptr.To("192.168.0.10/24"), + }, + index: 1, + expectedIP: "192.168.0.12", + }), + Entry("Start unset, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: ptr.To("192.168.0.10/24"), + }, + index: 250, + expectError: true, + }), + ) + + type testCaseAddOffsetToIP struct { + ip string + endIP string + offset int + expectedIP string + expectError bool + } + + DescribeTable("Test AddOffsetToIP", + func(tc testCaseAddOffsetToIP) { + testIP := net.ParseIP(tc.ip) + testEndIP := net.ParseIP(tc.endIP) + expectedIP := net.ParseIP(tc.expectedIP) + + result, err := addOffsetToIP(testIP, testEndIP, tc.offset) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(expectedIP)) + } + }, + Entry("valid IPv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 10, + expectedIP: "192.168.0.20", + }), + Entry("valid IPv4, no end ip", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + offset: 1000, + expectedIP: "192.168.3.242", + }), + Entry("Over bound ipv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 1000, + expectError: true, + }), + Entry("error ipv4", testCaseAddOffsetToIP{ + ip: "255.255.255.250", + offset: 10, + expectError: true, + }), + Entry("valid IPv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::fff0", + offset: 10, + expectedIP: "2001::1A", + }), + Entry("valid IPv6, no end ip", testCaseAddOffsetToIP{ + ip: "2001::10", + offset: 10000, + expectedIP: "2001::2720", + }), + Entry("Over bound ipv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::00f0", + offset: 10000, + expectError: true, + }), + Entry("error ipv6", testCaseAddOffsetToIP{ + ip: "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFF0", + offset: 100, + expectError: true, + }), + ) + +}) diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 00000000..9bf5fac0 --- /dev/null +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,380 @@ +//go:build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPAddress) DeepCopyInto(out *IPAddress) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddress. +func (in *IPAddress) DeepCopy() *IPAddress { + if in == nil { + return nil + } + out := new(IPAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPAddress) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPAddressList) DeepCopyInto(out *IPAddressList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IPAddress, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressList. +func (in *IPAddressList) DeepCopy() *IPAddressList { + if in == nil { + return nil + } + out := new(IPAddressList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPAddressList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPAddressSpec) DeepCopyInto(out *IPAddressSpec) { + *out = *in + out.Claim = in.Claim + out.Pool = in.Pool + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(string) + **out = **in + } + if in.DNSServers != nil { + in, out := &in.DNSServers, &out.DNSServers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressSpec. +func (in *IPAddressSpec) DeepCopy() *IPAddressSpec { + if in == nil { + return nil + } + out := new(IPAddressSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPClaim) DeepCopyInto(out *IPClaim) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaim. +func (in *IPClaim) DeepCopy() *IPClaim { + if in == nil { + return nil + } + out := new(IPClaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPClaim) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPClaimList) DeepCopyInto(out *IPClaimList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IPClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimList. +func (in *IPClaimList) DeepCopy() *IPClaimList { + if in == nil { + return nil + } + out := new(IPClaimList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPClaimList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPClaimSpec) DeepCopyInto(out *IPClaimSpec) { + *out = *in + out.Pool = in.Pool +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimSpec. +func (in *IPClaimSpec) DeepCopy() *IPClaimSpec { + if in == nil { + return nil + } + out := new(IPClaimSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPClaimStatus) DeepCopyInto(out *IPClaimStatus) { + *out = *in + if in.Address != nil { + in, out := &in.Address, &out.Address + *out = new(v1.ObjectReference) + **out = **in + } + if in.ErrorMessage != nil { + in, out := &in.ErrorMessage, &out.ErrorMessage + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPClaimStatus. +func (in *IPClaimStatus) DeepCopy() *IPClaimStatus { + if in == nil { + return nil + } + out := new(IPClaimStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPPool) DeepCopyInto(out *IPPool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool. +func (in *IPPool) DeepCopy() *IPPool { + if in == nil { + return nil + } + out := new(IPPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPPool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPPoolList) DeepCopyInto(out *IPPoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IPPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolList. +func (in *IPPoolList) DeepCopy() *IPPoolList { + if in == nil { + return nil + } + out := new(IPPoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IPPoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { + *out = *in + if in.ClusterName != nil { + in, out := &in.ClusterName, &out.ClusterName + *out = new(string) + **out = **in + } + if in.Pools != nil { + in, out := &in.Pools, &out.Pools + *out = make([]Pool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PreAllocations != nil { + in, out := &in.PreAllocations, &out.PreAllocations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(string) + **out = **in + } + if in.DNSServers != nil { + in, out := &in.DNSServers, &out.DNSServers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolSpec. +func (in *IPPoolSpec) DeepCopy() *IPPoolSpec { + if in == nil { + return nil + } + out := new(IPPoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPPoolStatus) DeepCopyInto(out *IPPoolStatus) { + *out = *in + if in.LastUpdated != nil { + in, out := &in.LastUpdated, &out.LastUpdated + *out = (*in).DeepCopy() + } + if in.Allocations != nil { + in, out := &in.Allocations, &out.Allocations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolStatus. +func (in *IPPoolStatus) DeepCopy() *IPPoolStatus { + if in == nil { + return nil + } + out := new(IPPoolStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Pool) DeepCopyInto(out *Pool) { + *out = *in + if in.Start != nil { + in, out := &in.Start, &out.Start + *out = new(string) + **out = **in + } + if in.End != nil { + in, out := &in.End, &out.End + *out = new(string) + **out = **in + } + if in.Subnet != nil { + in, out := &in.Subnet, &out.Subnet + *out = new(string) + **out = **in + } + if in.Gateway != nil { + in, out := &in.Gateway, &out.Gateway + *out = new(string) + **out = **in + } + if in.DNSServers != nil { + in, out := &in.DNSServers, &out.DNSServers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pool. +func (in *Pool) DeepCopy() *Pool { + if in == nil { + return nil + } + out := new(Pool) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/ipam.metal3.io_ipaddresses.yaml b/config/crd/bases/ipam.metal3.io_ipaddresses.yaml index bf50a2d6..728b8ab0 100644 --- a/config/crd/bases/ipam.metal3.io_ipaddresses.yaml +++ b/config/crd/bases/ipam.metal3.io_ipaddresses.yaml @@ -150,5 +150,128 @@ spec: type: object type: object served: true + storage: false + subresources: {} + - additionalPrinterColumns: + - description: Time duration since creation of Metal3IPAddress + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: IPAddress is the Schema for the ipaddresses API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IPAddressSpec defines the desired state of IPAddress. + properties: + address: + description: Address contains the IP address + type: string + claim: + description: Claim points to the object the IPClaim was created for. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + dnsServers: + description: DNSServers is the list of dns servers + items: + type: string + type: array + gateway: + description: Gateway is the gateway ip address + type: string + pool: + description: Pool is the IPPool this was generated from. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + prefix: + description: Prefix is the mask of the network as integer (max 128) + maximum: 128 + type: integer + required: + - address + - claim + - pool + type: object + type: object + served: true storage: true subresources: {} diff --git a/config/crd/bases/ipam.metal3.io_ipclaims.yaml b/config/crd/bases/ipam.metal3.io_ipclaims.yaml index 5c4f4cd4..2717c26e 100644 --- a/config/crd/bases/ipam.metal3.io_ipclaims.yaml +++ b/config/crd/bases/ipam.metal3.io_ipclaims.yaml @@ -137,6 +137,121 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: Time duration since creation of Metal3IPClaim + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: IPClaim is the Schema for the ipclaims API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IPClaimSpec defines the desired state of IPClaim. + properties: + pool: + description: Pool is the IPPool this was generated from. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - pool + type: object + status: + description: IPClaimStatus defines the observed state of IPClaim. + properties: + address: + description: Address is the IPAddress that was generated for this + claim. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + errorMessage: + description: ErrorMessage contains the error message + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/ipam.metal3.io_ippools.yaml b/config/crd/bases/ipam.metal3.io_ippools.yaml index ed08b305..bfd82d18 100644 --- a/config/crd/bases/ipam.metal3.io_ippools.yaml +++ b/config/crd/bases/ipam.metal3.io_ippools.yaml @@ -146,6 +146,117 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: Cluster to which this template belongs + jsonPath: .metadata.labels.cluster\.x-k8s\.io/cluster-name + name: Cluster + type: string + - description: Time duration since creation of Metal3IPPool + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: IPPool is the Schema for the ippools API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IPPoolSpec defines the desired state of IPPool. + properties: + clusterName: + description: ClusterName is the name of the Cluster this object belongs + to. + type: string + dnsServers: + description: DNSServers is the list of dns servers + items: + type: string + type: array + gateway: + description: Gateway is the gateway ip address + type: string + namePrefix: + description: namePrefix is the prefix used to generate the IPAddress + object names + minLength: 1 + type: string + pools: + description: Pools contains the list of IP addresses pools + items: + description: MetaDataIPAddress contains the info to render th ip + address. It is IP-version agnostic. + properties: + dnsServers: + description: DNSServers is the list of dns servers + items: + type: string + type: array + end: + description: End is the last IP address that can be rendered. + It is used as a validation that the rendered IP is in bound. + type: string + gateway: + description: Gateway is the gateway ip address + type: string + prefix: + description: Prefix is the mask of the network as integer (max + 128) + maximum: 128 + type: integer + start: + description: Start is the first ip address that can be rendered + type: string + subnet: + description: Subnet is used to validate that the rendered IP + is in bounds. In case the Start value is not given, it is + derived from the subnet ip incremented by 1 (`192.168.0.1` + for `192.168.0.0/24`) + type: string + type: object + type: array + preAllocations: + additionalProperties: + type: string + description: PreAllocations contains the preallocated IP addresses + type: object + prefix: + description: Prefix is the mask of the network as integer (max 128) + maximum: 128 + type: integer + required: + - namePrefix + type: object + status: + description: IPPoolStatus defines the observed state of IPPool. + properties: + indexes: + additionalProperties: + type: string + description: Allocations contains the map of objects and IP addresses + they have + type: object + lastUpdated: + description: LastUpdated identifies when this status was last observed. + format: date-time + type: string + type: object + type: object + served: true storage: true subresources: status: {} 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/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index e6d64844..e887bc48 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -70,6 +70,72 @@ webhooks: resources: - ippools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-ipam-metal3-io-v1alpha2-ipaddress + failurePolicy: Fail + matchPolicy: Equivalent + name: default.ipaddress.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ipaddresses + sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-ipam-metal3-io-v1alpha2-ipclaim + failurePolicy: Fail + matchPolicy: Equivalent + name: default.ipclaim.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ipclaims + sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-ipam-metal3-io-v1alpha2-ippool + failurePolicy: Fail + matchPolicy: Equivalent + name: default.ippool.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ippools + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -142,3 +208,69 @@ webhooks: resources: - ippools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-ipam-metal3-io-v1alpha2-ipaddress + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.ipaddress.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ipaddresses + sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-ipam-metal3-io-v1alpha2-ipclaim + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.ipclaim.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ipclaims + sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-ipam-metal3-io-v1alpha2-ippool + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.ippool.ipam.metal3.io + rules: + - apiGroups: + - ipam.metal3.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - ippools + sideEffects: None diff --git a/controllers/ippool_controller.go b/controllers/ippool_controller.go index c0d919ce..97a91fac 100644 --- a/controllers/ippool_controller.go +++ b/controllers/ippool_controller.go @@ -21,12 +21,13 @@ import ( "time" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "github.com/metal3-io/ip-address-manager/ipam" "github.com/pkg/errors" 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 @@ -65,42 +70,42 @@ func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ c metadataLog := r.Log.WithName(ipPoolControllerName).WithValues("metal3-ippool", req.NamespacedName) // Fetch the IPPool instance. - ipamv1IPPool := &ipamv1.IPPool{} + ipamv2IPPool := &ipamv2.IPPool{} - if err := r.Client.Get(ctx, req.NamespacedName, ipamv1IPPool); err != nil { + if err := r.Client.Get(ctx, req.NamespacedName, ipamv2IPPool); err != nil { if apierrors.IsNotFound(err) { return ctrl.Result{}, nil } return ctrl.Result{}, err } - helper, err := patch.NewHelper(ipamv1IPPool, r.Client) + helper, err := patch.NewHelper(ipamv2IPPool, r.Client) if err != nil { return ctrl.Result{}, errors.Wrap(err, "failed to init patch helper") } - // Always patch ipamv1IPPool exiting this function so we can persist any IPPool changes. + // Always patch ipamv2IPPool exiting this function so we can persist any IPPool changes. defer func() { - err := helper.Patch(ctx, ipamv1IPPool) + err := helper.Patch(ctx, ipamv2IPPool) if err != nil { - metadataLog.Info("failed to Patch ipamv1IPPool") + metadataLog.Info("failed to Patch ipamv2IPPool") } }() cluster := &clusterv1.Cluster{} - if ipamv1IPPool.Spec.ClusterName != nil { + if ipamv2IPPool.Spec.ClusterName != nil { key := client.ObjectKey{ - Name: *ipamv1IPPool.Spec.ClusterName, - Namespace: ipamv1IPPool.Namespace, + Name: *ipamv2IPPool.Spec.ClusterName, + Namespace: ipamv2IPPool.Namespace, } - if ipamv1IPPool.ObjectMeta.Labels == nil { - ipamv1IPPool.ObjectMeta.Labels = make(map[string]string) + if ipamv2IPPool.ObjectMeta.Labels == nil { + ipamv2IPPool.ObjectMeta.Labels = make(map[string]string) } - ipamv1IPPool.ObjectMeta.Labels[clusterv1.ClusterNameLabel] = *ipamv1IPPool.Spec.ClusterName - ipamv1IPPool.ObjectMeta.Labels[clusterv1.ProviderNameLabel] = "infrastructure-metal3" + ipamv2IPPool.ObjectMeta.Labels[clusterv1.ClusterNameLabel] = *ipamv2IPPool.Spec.ClusterName + ipamv2IPPool.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) - if ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { + if ipamv2IPPool.ObjectMeta.DeletionTimestamp.IsZero() { if err != nil { metadataLog.Info("Error fetching cluster. It might not exist yet, Requeuing") return ctrl.Result{}, nil @@ -111,26 +116,26 @@ func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ c } // Create a helper for managing the metadata object. - ipPoolMgr, err := r.ManagerFactory.NewIPPoolManager(ipamv1IPPool, metadataLog) + ipPoolMgr, err := r.ManagerFactory.NewIPPoolManager(ipamv2IPPool, metadataLog) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the IP pool") } - if ipamv1IPPool.Spec.ClusterName != nil && cluster != nil && cluster.Name != "" { + if ipamv2IPPool.Spec.ClusterName != nil && cluster != nil && cluster.Name != "" { metadataLog = metadataLog.WithValues("cluster", cluster.Name) if err := ipPoolMgr.SetClusterOwnerRef(cluster); err != nil { return ctrl.Result{}, err } // Return early if the Metadata or Cluster is paused. - if annotations.IsPaused(cluster, ipamv1IPPool) { + if annotations.IsPaused(cluster, ipamv2IPPool) { metadataLog.Info("reconciliation is paused for this object") return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil } } // Handle deleted metadata - if !ipamv1IPPool.ObjectMeta.DeletionTimestamp.IsZero() { + if !ipamv2IPPool.ObjectMeta.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, ipPoolMgr) } @@ -170,23 +175,36 @@ 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{}). + For(&ipamv2.IPPool{}). WithOptions(options). Watches( - &ipamv1.IPClaim{}, + &ipamv2.IPClaim{}, handler.EnqueueRequestsFromMapFunc(r.IPClaimToIPPool), ). WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). 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(&ipamv2.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. func (r *IPPoolReconciler) IPClaimToIPPool(_ context.Context, obj client.Object) []ctrl.Request { - if m3ipc, ok := obj.(*ipamv1.IPClaim); ok { + if m3ipc, ok := obj.(*ipamv2.IPClaim); ok { if m3ipc.Spec.Pool.Name != "" { namespace := m3ipc.Spec.Pool.Namespace if namespace == "" { @@ -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..6e94dfba 100644 --- a/controllers/ippool_controller_test.go +++ b/controllers/ippool_controller_test.go @@ -24,7 +24,7 @@ import ( . "github.com/onsi/gomega" "github.com/golang/mock/gomock" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "github.com/metal3-io/ip-address-manager/ipam" ipam_mocks "github.com/metal3-io/ip-address-manager/ipam/mocks" "github.com/pkg/errors" @@ -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" @@ -51,7 +52,7 @@ var _ = Describe("IPPool controller", func() { expectError bool expectRequeue bool expectManager bool - m3ipp *ipamv1.IPPool + m3ipp *ipamv2.IPPool cluster *clusterv1.Cluster managerError bool reconcileNormal bool @@ -137,45 +138,45 @@ var _ = Describe("IPPool controller", func() { }, Entry("IPPool not found", testCaseReconcile{}), Entry("Cluster not found", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, }), Entry("Deletion, Cluster not found", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", DeletionTimestamp: ×tampNow, Finalizers: []string{ - ipamv1.IPClaimFinalizer, + ipamv2.IPClaimFinalizer, }, }, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, expectManager: true, }), Entry("Deletion, Cluster not found, error", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", DeletionTimestamp: ×tampNow, Finalizers: []string{ - ipamv1.IPClaimFinalizer, + ipamv2.IPClaimFinalizer, }, }, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, expectManager: true, reconcileDeleteError: true, expectError: true, }), Entry("Paused cluster", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, cluster: &clusterv1.Cluster{ ObjectMeta: testObjectMeta, @@ -187,9 +188,9 @@ var _ = Describe("IPPool controller", func() { expectManager: true, }), Entry("Error in manager", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, cluster: &clusterv1.Cluster{ ObjectMeta: testObjectMeta, @@ -197,9 +198,9 @@ var _ = Describe("IPPool controller", func() { managerError: true, }), Entry("Reconcile normal error", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, cluster: &clusterv1.Cluster{ ObjectMeta: testObjectMeta, @@ -209,17 +210,17 @@ var _ = Describe("IPPool controller", func() { expectManager: true, }), Entry("Reconcile normal no cluster", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, reconcileNormal: false, expectManager: false, }), Entry("Reconcile normal no error", testCaseReconcile{ - m3ipp: &ipamv1.IPPool{ + m3ipp: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ClusterName: ptr.To("abc")}, + Spec: ipamv2.IPPoolSpec{ClusterName: ptr.To("abc")}, }, cluster: &clusterv1.Cluster{ ObjectMeta: testObjectMeta, @@ -341,7 +342,7 @@ var _ = Describe("IPPool controller", func() { ) type TestCaseM3IPCToM3IPP struct { - IPClaim *ipamv1.IPClaim + IPClaim *ipamv2.IPClaim ExpectRequest bool } @@ -372,18 +373,18 @@ var _ = Describe("IPPool controller", func() { }, Entry("No IPPool in Spec", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.IPClaim{ + IPClaim: &ipamv2.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPClaimSpec{}, + Spec: ipamv2.IPClaimSpec{}, }, ExpectRequest: false, }, ), Entry("IPPool in Spec, with namespace", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.IPClaim{ + IPClaim: &ipamv2.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -395,9 +396,9 @@ var _ = Describe("IPPool controller", func() { ), Entry("IPPool in Spec, no namespace", TestCaseM3IPCToM3IPP{ - IPClaim: &ipamv1.IPClaim{ + IPClaim: &ipamv2.IPClaim{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", }, @@ -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..f07cf284 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -29,9 +29,10 @@ import ( "k8s.io/client-go/rest" "k8s.io/klog/v2" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" 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" @@ -56,7 +57,9 @@ func init() { // Register required object kinds with global scheme. _ = apiextensionsv1.AddToScheme(scheme.Scheme) _ = clusterv1.AddToScheme(scheme.Scheme) - _ = ipamv1.AddToScheme(scheme.Scheme) + _ = ipamv2.AddToScheme(scheme.Scheme) + _ = capipamv1.AddToScheme(scheme.Scheme) + } func setupScheme() *runtime.Scheme { @@ -66,7 +69,11 @@ func setupScheme() *runtime.Scheme { panic(err) } - if err := ipamv1.AddToScheme(s); err != nil { + if err := ipamv2.AddToScheme(s); err != nil { + panic(err) + } + + if err := capipamv1.AddToScheme(s); err != nil { panic(err) } @@ -91,7 +98,10 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - err = ipamv1.AddToScheme(scheme.Scheme) + err = ipamv2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = capipamv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = apiextensionsv1.AddToScheme(scheme.Scheme) 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..9f2bd60f 100644 --- a/examples/ippool/ippool.yaml +++ b/examples/ippool/ippool.yaml @@ -22,7 +22,7 @@ spec: name: ${CLUSTER_NAME}-controlplane namespace: ${NAMESPACE} --- -apiVersion: ipam.metal3.io/v1alpha1 +apiVersion: ipam.metal3.io/v1alpha2 kind: IPPool metadata: name: pool1 @@ -31,7 +31,7 @@ spec: clusterName: ${CLUSTER_NAME} namePrefix: ${CLUSTER_NAME}-prov --- -apiVersion: ipam.metal3.io/v1alpha1 +apiVersion: ipam.metal3.io/v1alpha2 kind: IPClaim metadata: name: ${CLUSTER_NAME}-controlplane-template-0-provisioning-pool @@ -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/ippool/kustomizeconfig.yaml b/examples/ippool/kustomizeconfig.yaml index 1d541ba0..23ecf0ab 100644 --- a/examples/ippool/kustomizeconfig.yaml +++ b/examples/ippool/kustomizeconfig.yaml @@ -1,6 +1,6 @@ namespace: - kind: IPClaim group: ipam.metal3.io - version: v1alpha1 + version: v1alpha2 path: spec/pool/namespace create: true 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..46f44021 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -22,18 +22,27 @@ import ( "strings" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" 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 { @@ -46,12 +55,12 @@ type IPPoolManagerInterface interface { // IPPoolManager is responsible for performing machine reconciliation. type IPPoolManager struct { client client.Client - IPPool *ipamv1.IPPool + IPPool *ipamv2.IPPool Log logr.Logger } // NewIPPoolManager returns a new helper for managing a ipPool object. -func NewIPPoolManager(client client.Client, ipPool *ipamv1.IPPool, ipPoolLog logr.Logger) (*IPPoolManager, error) { +func NewIPPoolManager(client client.Client, ipPool *ipamv2.IPPool, ipPoolLog logr.Logger) (*IPPoolManager, error) { return &IPPoolManager{ client: client, IPPool: ipPool, @@ -62,9 +71,9 @@ func NewIPPoolManager(client client.Client, ipPool *ipamv1.IPPool, ipPoolLog log // SetFinalizer sets finalizer. func (m *IPPoolManager) SetFinalizer() { // If the Metal3Machine doesn't have finalizer, add it. - if !Contains(m.IPPool.Finalizers, ipamv1.IPPoolFinalizer) { + if !Contains(m.IPPool.Finalizers, ipamv2.IPPoolFinalizer) { m.IPPool.Finalizers = append(m.IPPool.Finalizers, - ipamv1.IPPoolFinalizer, + ipamv2.IPPoolFinalizer, ) } } @@ -73,7 +82,7 @@ func (m *IPPoolManager) SetFinalizer() { func (m *IPPoolManager) UnsetFinalizer() { // Remove the finalizer. m.IPPool.Finalizers = Filter(m.IPPool.Finalizers, - ipamv1.IPPoolFinalizer, + ipamv2.IPPoolFinalizer, ) } @@ -101,16 +110,16 @@ func (m *IPPoolManager) SetClusterOwnerRef(cluster *clusterv1.Cluster) error { } // RecreateStatus recreates the status if empty. -func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr]string, error) { +func (m *IPPoolManager) getIndexes(ctx context.Context) (map[string]string, error) { m.Log.Info("Fetching IPAddress objects") // start from empty maps if m.IPPool.Status.Allocations == nil { - m.IPPool.Status.Allocations = make(map[string]ipamv1.IPAddressStr) + m.IPPool.Status.Allocations = make(map[string]string) } - updatedAllocations := make(map[string]ipamv1.IPAddressStr) + updatedAllocations := make(map[string]string) - addresses := make(map[ipamv1.IPAddressStr]string) + addresses := make(map[string]string) // After addresses map is populated, we consider that there are still addresses in use. // However, when IPPool.Spec.PreAllocations is given, it can still hold addresses even @@ -122,13 +131,13 @@ func (m *IPPoolManager) getIndexes(ctx context.Context) (map[ipamv1.IPAddressStr } } - // get list of IPAddress objects - addressObjects := ipamv1.IPAddressList{} // without this ListOption, all namespaces would be including in the listing opts := &client.ListOptions{ Namespace: m.IPPool.Namespace, } + // get list of IPAddress objects for metal3.io addresses + addressObjects := ipamv2.IPAddressList{} err := m.client.List(ctx, &addressObjects, opts) if err != nil { return addresses, err @@ -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] = addressObject.Spec.Address + addresses[addressObject.Spec.Address] = claimName + } + if !reflect.DeepEqual(updatedAllocations, m.IPPool.Status.Allocations) { m.IPPool.Status.Allocations = updatedAllocations m.updateStatusTimestamp() @@ -167,16 +203,29 @@ func (m *IPPoolManager) updateStatusTimestamp() { m.IPPool.Status.LastUpdated = &now } -// 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 } // get list of IPClaim objects - addressClaimObjects := ipamv1.IPClaimList{} + addressClaimObjects := ipamv2.IPClaimList{} // without this ListOption, all namespaces would be including in the listing opts := &client.ListOptions{ Namespace: m.IPPool.Namespace, @@ -207,9 +256,48 @@ 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) { + addressClaim *ipamv2.IPClaim, addresses map[string]string, +) (map[string]string, error) { helper, err := patch.NewHelper(addressClaim, m.client) if err != nil { return addresses, errors.Wrap(err, "failed to init patch helper") @@ -233,7 +321,7 @@ func (m *IPPoolManager) updateAddress(ctx context.Context, // 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, ipamv1.IPClaimFinalizer)) { + (len(addressClaim.Finalizers) == 1 && !Contains(addressClaim.Finalizers, ipamv2.IPClaimFinalizer)) { m.Log.Info("IPClaim is still in use (has other finalizers). Cannot delete IPAddress.", "IPClaim", addressClaim.Name, "Finalizers", addressClaim.Finalizers) return addresses, nil @@ -247,10 +335,59 @@ func (m *IPPoolManager) updateAddress(ctx context.Context, return addresses, nil } -func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, - addresses map[ipamv1.IPAddressStr]string, -) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, []ipamv1.IPAddressStr, error) { - var allocatedAddress ipamv1.IPAddressStr +func (m *IPPoolManager) K8sUpdateAddress(ctx context.Context, + addressClaim *capipamv1.IPAddressClaim, addresses map[string]string, +) (map[string]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 *ipamv2.IPClaim, + addresses map[string]string, +) (string, int, *string, []string, error) { + var allocatedAddress string var err error // Get pre-allocated addresses @@ -268,7 +405,7 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, } index := 0 for !ipAllocated { - allocatedAddress, err = ipamv1.GetIPAddress(pool, index) + allocatedAddress, err = ipamv2.GetIPAddress(pool, index) if err != nil { break } @@ -306,21 +443,109 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, // misconfigured if !ipAllocated && ipPreAllocated { addressClaim.Status.ErrorMessage = ptr.To("Pre-allocated IP out of bond") - return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("Pre-allocated IP out of bond") + return "", 0, nil, []string{}, errors.New("Pre-allocated IP out of bond") } if !ipAllocated { addressClaim.Status.ErrorMessage = ptr.To("Exhausted IP Pools") - return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("Exhausted IP Pools") + return "", 0, nil, []string{}, errors.New("Exhausted IP Pools") } return allocatedAddress, prefix, gateway, dnsServers, nil } +func (m *IPPoolManager) K8sAllocateAddress(addressClaim *capipamv1.IPAddressClaim, + addresses map[string]string, +) (string, int, string, error) { + var allocatedAddress string + 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 = ipamv2.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, "", 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, "", errors.New("Exhausted IP Pools") + } + var g string + if gateway != nil { + g = *gateway + } else { + g = "" + } + return allocatedAddress, prefix, g, nil +} + func (m *IPPoolManager) createAddress(ctx context.Context, - addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, -) (map[ipamv1.IPAddressStr]string, error) { - if !Contains(addressClaim.Finalizers, ipamv1.IPClaimFinalizer) { + addressClaim *ipamv2.IPClaim, addresses map[string]string, +) (map[string]string, error) { + if !Contains(addressClaim.Finalizers, ipamv2.IPClaimFinalizer) { addressClaim.Finalizers = append(addressClaim.Finalizers, - ipamv1.IPClaimFinalizer, + ipamv2.IPClaimFinalizer, ) } @@ -363,19 +588,19 @@ func (m *IPPoolManager) createAddress(ctx context.Context, // Create the IPAddress object, with an Owner ref to the IPClaim, // the IPPool, and the IPClaims owners. Also add a finalizer. - addressObject := &ipamv1.IPAddress{ + addressObject := &ipamv2.IPAddress{ TypeMeta: metav1.TypeMeta{ Kind: "IPAddress", - APIVersion: ipamv1.GroupVersion.String(), + APIVersion: ipamv2.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: addressName, Namespace: m.IPPool.Namespace, - Finalizers: []string{ipamv1.IPAddressFinalizer}, + Finalizers: []string{ipamv2.IPAddressFinalizer}, OwnerReferences: ownerRefs, Labels: addressClaim.Labels, }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Address: allocatedAddress, Pool: corev1.ObjectReference{ Name: m.IPPool.Name, @@ -413,16 +638,122 @@ func (m *IPPoolManager) createAddress(ctx context.Context, return addresses, nil } +func (m *IPPoolManager) K8sCreateAddress(ctx context.Context, + addressClaim *capipamv1.IPAddressClaim, addresses map[string]string, +) (map[string]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 + } + + // 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: allocatedAddress, + PoolRef: corev1.TypedLocalObjectReference{ + Name: m.IPPool.Name, + Kind: m.IPPool.Kind, + APIGroup: &APIGroup, + }, + ClaimRef: corev1.LocalObjectReference{ + Name: addressClaim.Name, + }, + Prefix: prefix, + Gateway: gateway, + }, + } + + // 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, -) (map[ipamv1.IPAddressStr]string, error) { + addressClaim *ipamv2.IPClaim, addresses map[string]string, +) (map[string]string, error) { m.Log.Info("Deleting IPAddress associated with IPClaim", "IPClaim", addressClaim.Name) allocatedAddress, ok := m.IPPool.Status.Allocations[addressClaim.Name] + m.Log.Info("Found IPAddress to delete: ", allocatedAddress) if ok { + m.Log.Info("Found IPAddress to delete: ", m.formatAddressName(allocatedAddress)) // Try to get the IPAddress. if it succeeds, delete it - ipAddress := &ipamv1.IPAddress{} + ipAddress := &ipamv2.IPAddress{} key := client.ObjectKey{ Name: m.formatAddressName(allocatedAddress), Namespace: m.IPPool.Namespace, @@ -434,7 +765,7 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, } else if err == nil { // Remove the finalizer ipAddress.Finalizers = Filter(ipAddress.Finalizers, - ipamv1.IPAddressFinalizer, + ipamv2.IPAddressFinalizer, ) err = updateObject(ctx, m.client, ipAddress) if err != nil && !apierrors.IsNotFound(err) { @@ -452,7 +783,7 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, } addressClaim.Status.Address = nil addressClaim.Finalizers = Filter(addressClaim.Finalizers, - ipamv1.IPClaimFinalizer, + ipamv2.IPClaimFinalizer, ) err := updateObject(ctx, m.client, addressClaim) if err != nil && !apierrors.IsNotFound(err) { @@ -471,9 +802,85 @@ 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[string]string, +) (map[string]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 { +func (m *IPPoolManager) formatAddressName(address string) string { return strings.TrimRight(m.IPPool.Spec.NamePrefix+"-"+strings.Replace( - strings.Replace(string(address), ":", "-", -1), ".", "-", -1, + strings.Replace(address, ":", "-", -1), ".", "-", -1, ), "-") } diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 0ba20ab6..37c7fc8b 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -24,11 +24,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" corev1 "k8s.io/api/core/v1" 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,11 +42,17 @@ var ( testObjectReference = &corev1.ObjectReference{ Name: "abc", } + localtestObjectReference = &corev1.LocalObjectReference{ + Name: "abc", + } + typedtestObjectReference = &corev1.TypedLocalObjectReference{ + Name: "abc", + } ) var _ = Describe("IPPool manager", func() { DescribeTable("Test Finalizers", - func(ipPool *ipamv1.IPPool) { + func(ipPool *ipamv2.IPPool) { ipPoolMgr, err := NewIPPoolManager(nil, ipPool, logr.Discard(), ) @@ -54,17 +61,17 @@ var _ = Describe("IPPool manager", func() { ipPoolMgr.SetFinalizer() Expect(ipPool.ObjectMeta.Finalizers).To(ContainElement( - ipamv1.IPPoolFinalizer, + ipamv2.IPPoolFinalizer, )) ipPoolMgr.UnsetFinalizer() Expect(ipPool.ObjectMeta.Finalizers).NotTo(ContainElement( - ipamv1.IPPoolFinalizer, + ipamv2.IPPoolFinalizer, )) }, - Entry("No finalizers", &ipamv1.IPPool{}), - Entry("Additional Finalizers", &ipamv1.IPPool{ + Entry("No finalizers", &ipamv2.IPPool{}), + Entry("Additional Finalizers", &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Finalizers: []string{"foo"}, }, @@ -73,7 +80,7 @@ var _ = Describe("IPPool manager", func() { type testCaseSetClusterOwnerRef struct { cluster *clusterv1.Cluster - ipPool *ipamv1.IPPool + ipPool *ipamv2.IPPool expectError bool } @@ -97,7 +104,7 @@ var _ = Describe("IPPool manager", func() { expectError: true, }), Entry("no previous ownerref", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.IPPool{ + ipPool: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -109,7 +116,7 @@ var _ = Describe("IPPool manager", func() { }, }), Entry("previous ownerref", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.IPPool{ + ipPool: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", OwnerReferences: []metav1.OwnerReference{ @@ -126,7 +133,7 @@ var _ = Describe("IPPool manager", func() { }, }), Entry("ownerref present", testCaseSetClusterOwnerRef{ - ipPool: &ipamv1.IPPool{ + ipPool: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", OwnerReferences: []metav1.OwnerReference{ @@ -148,11 +155,12 @@ var _ = Describe("IPPool manager", func() { ) type testGetIndexes struct { - ipPool *ipamv1.IPPool - addresses []*ipamv1.IPAddress + ipPool *ipamv2.IPPool + addresses []*ipamv2.IPAddress + capiAddresses []*capipamv1.IPAddress expectError bool - expectedAddresses map[ipamv1.IPAddressStr]string - expectedAllocations map[string]ipamv1.IPAddressStr + expectedAddresses map[string]string + expectedAllocations map[string]string } DescribeTable("Test getIndexes", @@ -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(), @@ -169,7 +180,7 @@ var _ = Describe("IPPool manager", func() { previousAllocations := tc.ipPool.Status.Allocations if previousAllocations == nil { - previousAllocations = make(map[string]ipamv1.IPAddressStr) + previousAllocations = make(map[string]string) } addressMap, err := ipPoolMgr.getIndexes(context.TODO()) @@ -188,26 +199,154 @@ var _ = Describe("IPPool manager", func() { }, Entry("No addresses", testGetIndexes{ - ipPool: &ipamv1.IPPool{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, + ipPool: &ipamv2.IPPool{}, + expectedAddresses: map[string]string{}, + expectedAllocations: map[string]string{}, + }), + Entry("metal3 addresses", testGetIndexes{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", + }, + }, + }, + addresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-0", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd1", + Pool: *testObjectReference, + Claim: *testObjectReference, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bbc-1", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd2", + Pool: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-2", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd3", + Pool: corev1.ObjectReference{}, + Claim: *testObjectReference, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-3", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd4", + Pool: corev1.ObjectReference{ + Namespace: "myns", + }, + Claim: corev1.ObjectReference{}, + }, + }, + }, + expectedAddresses: map[string]string{ + "abcd1": "abc", + "bcde": "", + }, + expectedAllocations: map[string]string{ + "abc": "abcd1", + }, + }), + Entry("capi addresses", testGetIndexes{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "cbcd": "cbcde", + }, + }, + }, + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-0", + 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", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "cbbc", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-2", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, + }, + }, + }, + expectedAddresses: map[string]string{ + "cabcd1": "abc", + "cbcde": "", + }, + expectedAllocations: map[string]string{ + "abc": "cabcd1", + }, }), - Entry("addresses", testGetIndexes{ - ipPool: &ipamv1.IPPool{ + Entry("metal3 addresses and capi addresses", testGetIndexes{ + ipPool: &ipamv2.IPPool{ ObjectMeta: testObjectMeta, - Spec: ipamv1.IPPoolSpec{ - PreAllocations: map[string]ipamv1.IPAddressStr{ - "bcd": ipamv1.IPAddressStr("bcde"), + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", + "cbcd": "cbcde", }, }, }, - addresses: []*ipamv1.IPAddress{ + addresses: []*ipamv2.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abc-0", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Address: "abcd1", Pool: *testObjectReference, Claim: *testObjectReference, @@ -218,7 +357,97 @@ var _ = Describe("IPPool manager", func() { Name: "bbc-1", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ + Address: "abcd2", + Pool: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "bbc", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-2", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd3", + Pool: corev1.ObjectReference{}, + Claim: *testObjectReference, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-3", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "abcd4", + Pool: corev1.ObjectReference{ + Namespace: "myns", + }, + Claim: corev1.ObjectReference{}, + }, + }, + }, + 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: "cabc-2", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, + }, + }, + }, + expectedAddresses: map[string]string{ + "abcd1": "abc", + "bcde": "", + "cbcde": "", + }, + expectedAllocations: map[string]string{ + "abc": "abcd1", + }, + }), + Entry("metal3 addresses and capi addresses 2", testGetIndexes{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: testObjectMeta, + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", + "cbcd": "cbcde", + }, + }, + }, + addresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bbc-1", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ Address: "abcd2", Pool: corev1.ObjectReference{ Name: "bbc", @@ -235,7 +464,7 @@ var _ = Describe("IPPool manager", func() { Name: "abc-2", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Address: "abcd3", Pool: corev1.ObjectReference{}, Claim: *testObjectReference, @@ -246,7 +475,7 @@ var _ = Describe("IPPool manager", func() { Name: "abc-3", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Address: "abcd4", Pool: corev1.ObjectReference{ Namespace: "myns", @@ -255,32 +484,138 @@ var _ = Describe("IPPool manager", func() { }, }, }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("abcd1"): "abc", - ipamv1.IPAddressStr("bcde"): "", + capiAddresses: []*capipamv1.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-0", + 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", + }, + ClaimRef: corev1.LocalObjectReference{ + Name: "cbbc", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cabc-2", + Namespace: "myns", + }, + Spec: capipamv1.IPAddressSpec{ + Address: "cabcd3", + PoolRef: corev1.TypedLocalObjectReference{}, + ClaimRef: *localtestObjectReference, + }, + }, + }, + expectedAddresses: map[string]string{ + "bcde": "", + "cabcd1": "abc", + "cbcde": "", + }, + expectedAllocations: map[string]string{ + "abc": "cabcd1", + }, + }), + Entry("IPPool with deletion timestamp (metal3 addresses)", testGetIndexes{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &timeNow, + }, + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", + }, + }, + }, + addresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "inUseClaim", + Namespace: "myns", + }, + Address: "192.168.1.11", + Gateway: ptr.To("192.168.0.1"), + Prefix: 24, + }, + }, + }, + expectedAddresses: map[string]string{}, + expectedAllocations: map[string]string{}, + }), + Entry("IPPool with deletion timestamp (capi addresses)", testGetIndexes{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &timeNow, + }, + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", + }, + }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("abcd1"), + 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[string]string{}, + expectedAllocations: map[string]string{}, }), - Entry("IPPool with deletion timestamp", testGetIndexes{ - ipPool: &ipamv1.IPPool{ + Entry("IPPool with deletion timestamp (metal3 and capi addresses)", testGetIndexes{ + ipPool: &ipamv2.IPPool{ ObjectMeta: metav1.ObjectMeta{ DeletionTimestamp: &timeNow, }, - Spec: ipamv1.IPPoolSpec{ - PreAllocations: map[string]ipamv1.IPAddressStr{ - "bcd": ipamv1.IPAddressStr("bcde"), + Spec: ipamv2.IPPoolSpec{ + PreAllocations: map[string]string{ + "bcd": "bcde", }, }, }, - addresses: []*ipamv1.IPAddress{ + addresses: []*ipamv2.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-11", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -289,14 +624,33 @@ var _ = Describe("IPPool manager", func() { Name: "inUseClaim", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Address: "192.168.1.11", + Gateway: ptr.To("192.168.0.1"), + Prefix: 24, + }, + }, + }, + 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, }, }, }, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAddresses: map[string]string{}, + expectedAllocations: map[string]string{}, }), ) @@ -306,13 +660,15 @@ var _ = Describe("IPPool manager", func() { } type testCaseUpdateAddresses struct { - ipPool *ipamv1.IPPool - ipClaims []*ipamv1.IPClaim - ipAddresses []*ipamv1.IPAddress + ipPool *ipamv2.IPPool + ipClaims []*ipamv2.IPClaim + ipAddresses []*ipamv2.IPAddress + ipAddressClaims []*capipamv1.IPAddressClaim + capiAddresses []*capipamv1.IPAddress expectRequeue bool expectError bool expectedNbAllocations int - expectedAllocations map[string]ipamv1.IPAddressStr + expectedAllocations map[string]string } DescribeTable("Test UpdateAddresses", @@ -324,6 +680,12 @@ var _ = Describe("IPPool manager", func() { 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(), @@ -346,7 +708,7 @@ var _ = Describe("IPPool manager", func() { Expect(tc.ipPool.Status.Allocations).To(Equal(tc.expectedAllocations)) // get list of IPAddress objects - addressObjects := ipamv1.IPClaimList{} + addressObjects := ipamv2.IPClaimList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) @@ -358,33 +720,45 @@ var _ = Describe("IPPool manager", func() { } } + // 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()) + } + } + }, Entry("No Claims", testCaseUpdateAddresses{ - ipPool: &ipamv1.IPPool{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, + expectedAllocations: map[string]string{}, }), Entry("Claim and IP exist", testCaseUpdateAddresses{ - ipPool: &ipamv1.IPPool{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ + Spec: ipamv2.IPPoolSpec{ NamePrefix: "abcpref", }, }, - ipClaims: []*ipamv1.IPClaim{ + ipClaims: []*ipamv2.IPClaim{ { ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-11", Namespace: "myns", @@ -396,13 +770,13 @@ var _ = Describe("IPPool manager", func() { Name: "abcd", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abcd", Namespace: "myns", }, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-12", Namespace: "myns", @@ -414,13 +788,13 @@ var _ = Describe("IPPool manager", func() { Name: "abce", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-12", Namespace: "myns", @@ -433,16 +807,16 @@ var _ = Describe("IPPool manager", func() { Namespace: "myns", DeletionTimestamp: &timeNow, Finalizers: []string{ - ipamv1.IPClaimFinalizer, + ipamv2.IPClaimFinalizer, }, }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", }, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{ Name: "abcpref-192-168-1-13", Namespace: "myns", @@ -450,13 +824,13 @@ var _ = Describe("IPPool manager", func() { }, }, }, - ipAddresses: []*ipamv1.IPAddress{ + ipAddresses: []*ipamv2.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-1-11", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -465,8 +839,8 @@ var _ = Describe("IPPool manager", func() { Name: "abc", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Address: "192.168.1.11", + Gateway: ptr.To("192.168.0.1"), Prefix: 24, }, }, @@ -475,7 +849,7 @@ var _ = Describe("IPPool manager", func() { Name: "abcpref-192-168-1-12", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -484,7 +858,7 @@ var _ = Describe("IPPool manager", func() { Name: "abce", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.12"), + Address: "192.168.1.12", }, }, { @@ -492,7 +866,7 @@ var _ = Describe("IPPool manager", func() { Name: "abcpref-192-168-1-13", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: ipamv2.IPAddressSpec{ Pool: corev1.ObjectReference{ Name: "abc", Namespace: "myns", @@ -501,85 +875,720 @@ var _ = Describe("IPPool manager", func() { Name: "abcf", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.13"), + Address: "192.168.1.13", }, }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.1.11"), - "abce": ipamv1.IPAddressStr("192.168.1.12"), + expectedAllocations: map[string]string{ + "abc": "192.168.1.11", + "abce": "192.168.1.12", }, expectedNbAllocations: 2, }), - Entry("IPClaim with deletion timestamp and finalizers", testCaseUpdateAddresses{ - ipPool: &ipamv1.IPPool{ + Entry("IPAddressClaim and IP exist", testCaseUpdateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ + Spec: ipamv2.IPPoolSpec{ NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{}, }, - ipClaims: []*ipamv1.IPClaim{ + ipAddressClaims: []*capipamv1.IPAddressClaim{ { ObjectMeta: metav1.ObjectMeta{ - Name: "inUseClaim", - Namespace: "myns", - DeletionTimestamp: &timeNow, - Finalizers: []string{ - ipamv1.IPClaimFinalizer, - "metal3data.infrastructure.cluster.x-k8s.io", - }, + Name: "abc", + Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + Status: capipamv1.IPAddressClaimStatus{ + AddressRef: corev1.LocalObjectReference{ + Name: "abcpref-192-168-1-11", }, }, }, - }, - ipAddresses: []*ipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ - Name: "abcpref-192-168-1-11", + Name: "abcd", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ - Pool: corev1.ObjectReference{ - Name: "abc", - Namespace: "myns", + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + Name: "abcd", }, - Claim: corev1.ObjectReference{ - Name: "inUseClaim", + }, + 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]string{ + "abc": "192.168.1.11", + "abce": "192.168.1.12", + }, + expectedNbAllocations: 2, + }), + Entry("Both IPClaim and IPAddressClaim and IP exist", testCaseUpdateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + NamePrefix: "abcpref", + }, + }, + ipClaims: []*ipamv2.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "myns", + }, + Spec: ipamv2.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", Namespace: "myns", }, - Address: ipamv1.IPAddressStr("192.168.1.11"), - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - Prefix: 24, }, + Status: ipamv2.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcd", + Namespace: "myns", + }, + Spec: ipamv2.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abcd", + Namespace: "myns", + }, + }, + Status: ipamv2.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abce", + Namespace: "myns", + }, + Spec: ipamv2.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv2.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-12", + Namespace: "myns", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcf", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv2.IPClaimFinalizer, + }, + }, + Spec: ipamv2.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + Status: ipamv2.IPClaimStatus{ + Address: &corev1.ObjectReference{ + Name: "abcpref-192-168-1-13", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + 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: ipamv2.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: ipamv2.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]string{ + "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: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{}, + }, + ipClaims: []*ipamv2.IPClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "inUseClaim", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv2.IPClaimFinalizer, + "metal3data.infrastructure.cluster.x-k8s.io", + }, + }, + Spec: ipamv2.IPClaimSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + }, + }, + }, + ipAddresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-1-11", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Pool: corev1.ObjectReference{ + Name: "abc", + Namespace: "myns", + }, + Claim: corev1.ObjectReference{ + Name: "inUseClaim", + Namespace: "myns", + }, + Address: "192.168.1.11", + Gateway: ptr.To("192.168.0.1"), + Prefix: 24, + }, + }, + }, + expectedAllocations: map[string]string{ + "inUseClaim": "192.168.1.11", + }, + expectedNbAllocations: 1, + }), + Entry("IPAddressClaim with deletion timestamp and finalizers", testCaseUpdateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{}, + }, + ipAddressClaims: []*capipamv1.IPAddressClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "inUseClaim", + Namespace: "myns", + DeletionTimestamp: &timeNow, + Finalizers: []string{ + ipamv2.IPClaimFinalizer, + "metal3data.infrastructure.cluster.x-k8s.io", + }, + }, + Spec: capipamv1.IPAddressClaimSpec{ + PoolRef: *typedtestObjectReference, + }, + }, + }, + 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, + }, + }, + }, + expectedAllocations: map[string]string{ + "inUseClaim": "192.168.1.11", + }, + expectedNbAllocations: 1, + }), + ) + + type testCaseCreateAddresses struct { + ipPool *ipamv2.IPPool + ipClaim *ipamv2.IPClaim + ipAddresses []*ipamv2.IPAddress + addresses map[string]string + expectRequeue bool + expectError bool + expectedIPAddresses []string + expectedAddresses map[string]string + expectedAllocations map[string]string + } + + 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 := ipamv2.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: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "abc": "foo-0", + }, + }, + }, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]string{ + "abc": "foo-0", + }, + }), + Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + }, + }, + PreAllocations: map[string]string{ + "abc": "192.168.0.15", + }, + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, + }, + }, + addresses: map[string]string{}, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]string{ + "abc": "192.168.0.15", + }, + expectedAddresses: map[string]string{ + "192.168.0.15": "abc", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-15"}, + }), + Entry("Not allocated yet", testCaseCreateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, + }, + }, + addresses: map[string]string{ + "192.168.0.11": "bcd", + }, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]string{ + "abc": "192.168.0.12", + }, + expectedAddresses: map[string]string{ + "192.168.0.12": "abc", + "192.168.0.11": "bcd", + }, + expectedIPAddresses: []string{"abcpref-192-168-0-12"}, + }), + Entry("Not allocated yet, conflict", testCaseCreateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, + }, + }, + addresses: map[string]string{}, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + ipAddresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abcpref-192-168-0-11", + Namespace: "myns", + }, + Spec: ipamv2.IPAddressSpec{ + Address: "192.168.0.11", + Pool: *testObjectReference, + Claim: corev1.ObjectReference{ + Name: "bcd", + }, + }, + }, + }, + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{}, + expectedIPAddresses: []string{"abcpref-192-168-0-11"}, + expectRequeue: true, + }), + Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ + ipPool: &ipamv2.IPPool{ + ObjectMeta: ipPoolMeta, + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.11"), + }, + }, + NamePrefix: "abcpref", + }, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "inUseClaim": ipamv1.IPAddressStr("192.168.1.11"), + addresses: map[string]string{ + "192.168.0.11": "bcd", }, - expectedNbAllocations: 1, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{ + "192.168.0.11": "bcd", + }, + expectedIPAddresses: []string{}, + expectError: true, }), ) - type testCaseCreateAddresses struct { - ipPool *ipamv1.IPPool - ipClaim *ipamv1.IPClaim - ipAddresses []*ipamv1.IPAddress - addresses map[ipamv1.IPAddressStr]string + type testCaseK8sCreateAddresses struct { + ipPool *ipamv2.IPPool + ipAddressClaim *capipamv1.IPAddressClaim + ipAddresses []*capipamv1.IPAddress + addresses map[string]string expectRequeue bool expectError bool expectedIPAddresses []string - expectedAddresses map[ipamv1.IPAddressStr]string - expectedAllocations map[string]ipamv1.IPAddressStr + expectedAddresses map[string]string + expectedAllocations map[string]string } - DescribeTable("Test CreateAddresses", - func(tc testCaseCreateAddresses) { + DescribeTable("Test K8sCreateAddresses", + func(tc testCaseK8sCreateAddresses) { objects := []client.Object{} for _, address := range tc.ipAddresses { objects = append(objects, address) @@ -590,7 +1599,7 @@ var _ = Describe("IPPool manager", func() { ) Expect(err).NotTo(HaveOccurred()) - allocatedMap, err := ipPoolMgr.createAddress(context.TODO(), tc.ipClaim, + allocatedMap, err := ipPoolMgr.K8sCreateAddress(context.TODO(), tc.ipAddressClaim, tc.addresses, ) if tc.expectRequeue || tc.expectError { @@ -604,7 +1613,7 @@ var _ = Describe("IPPool manager", func() { Expect(err).NotTo(HaveOccurred()) } // get list of IPAddress objects - addressObjects := ipamv1.IPAddressList{} + addressObjects := capipamv1.IPAddressList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) @@ -615,166 +1624,163 @@ var _ = Describe("IPPool manager", func() { 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{ + Entry("Already exists", testCaseK8sCreateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("foo-0"), + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "abc": "foo-0", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("foo-0"), + expectedAllocations: map[string]string{ + "abc": "foo-0", }, }), - Entry("Not allocated yet, pre-allocated", testCaseCreateAddresses{ - ipPool: &ipamv1.IPPool{ + Entry("Not allocated yet, pre-allocated", testCaseK8sCreateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), }, }, - PreAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.15"), + PreAllocations: map[string]string{ + "abc": "192.168.0.15", }, NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, }, }, - addresses: map[ipamv1.IPAddressStr]string{}, - ipClaim: &ipamv1.IPClaim{ + addresses: map[string]string{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.15"), + expectedAllocations: map[string]string{ + "abc": "192.168.0.15", }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.15"): "abc", + expectedAddresses: map[string]string{ + "192.168.0.15": "abc", }, expectedIPAddresses: []string{"abcpref-192-168-0-15"}, }), - Entry("Not allocated yet", testCaseCreateAddresses{ - ipPool: &ipamv1.IPPool{ + Entry("Not allocated yet", testCaseK8sCreateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + addresses: map[string]string{ + "192.168.0.11": "bcd", }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{ - "abc": ipamv1.IPAddressStr("192.168.0.12"), + expectedAllocations: map[string]string{ + "abc": "192.168.0.12", }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.12"): "abc", - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + expectedAddresses: map[string]string{ + "192.168.0.12": "abc", + "192.168.0.11": "bcd", }, expectedIPAddresses: []string{"abcpref-192-168-0-12"}, }), - Entry("Not allocated yet, conflict", testCaseCreateAddresses{ - ipPool: &ipamv1.IPPool{ + Entry("Not allocated yet, conflict", testCaseK8sCreateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, }, }, - addresses: map[ipamv1.IPAddressStr]string{}, - ipClaim: &ipamv1.IPClaim{ + addresses: map[string]string{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - ipAddresses: []*ipamv1.IPAddress{ + ipAddresses: []*capipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abcpref-192-168-0-11", Namespace: "myns", }, - Spec: ipamv1.IPAddressSpec{ + Spec: capipamv1.IPAddressSpec{ Address: "192.168.0.11", - Pool: corev1.ObjectReference{ - Name: "abc", - }, - Claim: corev1.ObjectReference{ + PoolRef: *typedtestObjectReference, + ClaimRef: corev1.LocalObjectReference{ Name: "bcd", }, }, }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{}, expectedIPAddresses: []string{"abcpref-192-168-0-11"}, expectRequeue: true, }), - Entry("Not allocated yet, exhausted pool", testCaseCreateAddresses{ - ipPool: &ipamv1.IPPool{ + Entry("Not allocated yet, exhausted pool", testCaseK8sCreateAddresses{ + ipPool: &ipamv2.IPPool{ ObjectMeta: ipPoolMeta, - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.11"), }, }, NamePrefix: "abcpref", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{}, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{}, }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + addresses: map[string]string{ + "192.168.0.11": "bcd", }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.11"): "bcd", + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{ + "192.168.0.11": "bcd", }, expectedIPAddresses: []string{}, expectError: true, @@ -782,13 +1788,13 @@ var _ = Describe("IPPool manager", func() { ) type testCaseAllocateAddress struct { - ipPool *ipamv1.IPPool - ipClaim *ipamv1.IPClaim - addresses map[ipamv1.IPAddressStr]string - expectedAddress ipamv1.IPAddressStr + ipPool *ipamv2.IPPool + ipClaim *ipamv2.IPClaim + addresses map[string]string + expectedAddress string expectedPrefix int - expectedGateway *ipamv1.IPAddressStr - expectedDNSServers []ipamv1.IPAddressStr + expectedGateway *string + expectedDNSServers []string expectError bool } @@ -812,10 +1818,10 @@ var _ = Describe("IPPool manager", func() { Expect(dnsServers).To(Equal(tc.expectedDNSServers)) }, Entry("Empty pools", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{}, + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{}, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", }, @@ -823,106 +1829,106 @@ var _ = Describe("IPPool manager", func() { expectError: true, }), Entry("One pool, pre-allocated", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: 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"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.8.8", }, }, { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.21")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.30")), + Start: ptr.To("192.168.0.21"), + End: ptr.To("192.168.0.30"), }, }, - PreAllocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.21"), + PreAllocations: map[string]string{ + "TestRef": "192.168.0.21", }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), + Gateway: ptr.To("192.168.0.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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"), + expectedAddress: "192.168.0.21", + expectedGateway: ptr.To("192.168.0.1"), + expectedDNSServers: []string{ + "8.8.4.4", }, expectedPrefix: 24, }), Entry("One pool, pre-allocated, with overrides", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: 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"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.8.8", }, }, }, - PreAllocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + PreAllocations: map[string]string{ + "TestRef": "192.168.0.15", }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), + Gateway: ptr.To("192.168.0.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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"), + expectedAddress: "192.168.0.15", + expectedGateway: ptr.To("192.168.1.1"), + expectedDNSServers: []string{ + "8.8.8.8", }, expectedPrefix: 26, }), Entry("One pool, pre-allocated, out of bonds", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: 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"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.8.8", }, }, }, - PreAllocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.21"), + PreAllocations: map[string]string{ + "TestRef": "192.168.0.21", }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), + Gateway: ptr.To("192.168.0.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, @@ -930,204 +1936,495 @@ var _ = Describe("IPPool manager", func() { expectError: true, }), Entry("One pool, with start and existing address", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), }, }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Gateway: ptr.To("192.168.0.1"), }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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", + addresses: map[string]string{ + "192.168.0.12": "bcde", + "192.168.0.11": "abcd", }, - expectedAddress: ipamv1.IPAddressStr("192.168.0.13"), - expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedAddress: "192.168.0.13", + expectedGateway: 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{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + Start: ptr.To("192.168.0.11"), + End: 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"), + Gateway: ptr.To("192.168.0.1"), + DNSServers: []string{ + "8.8.8.8", }, }, }, Prefix: 26, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.1.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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", + addresses: map[string]string{ + "192.168.0.12": "bcde", + "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"), + expectedAddress: "192.168.0.13", + expectedGateway: ptr.To("192.168.0.1"), + expectedDNSServers: []string{ + "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{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + Start: ptr.To("192.168.0.10"), + End: 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"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.8.8", }, }, { - Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), + Subnet: 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"), + Gateway: ptr.To("192.168.2.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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", + addresses: map[string]string{ + "192.168.1.11": "bcde", + "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"), + expectedAddress: "192.168.1.12", + expectedGateway: ptr.To("192.168.2.1"), + expectedDNSServers: []string{ + "8.8.4.4", }, expectedPrefix: 26, }), Entry("two pools, with subnet and override prefix", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.10"), }, { - Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.1.10/24")), + Subnet: 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"), + Gateway: ptr.To("192.168.1.1"), + DNSServers: []string{ + "8.8.8.8", }, }, }, Prefix: 26, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.2.1")), - DNSServers: []ipamv1.IPAddressStr{ - ipamv1.IPAddressStr("8.8.4.4"), + Gateway: ptr.To("192.168.2.1"), + DNSServers: []string{ + "8.8.4.4", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.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", + addresses: map[string]string{ + "192.168.1.11": "bcde", + "192.168.0.10": "abcd", }, - 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"), + expectedAddress: "192.168.1.12", + expectedGateway: ptr.To("192.168.1.1"), + expectedDNSServers: []string{ + "8.8.8.8", }, expectedPrefix: 24, }), Entry("Exhausted pools start", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), - End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.10")), + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.10"), }, }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Gateway: ptr.To("192.168.0.1"), }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.10"): "abcd", + addresses: map[string]string{ + "192.168.0.10": "abcd", }, expectError: true, }), Entry("Exhausted pools subnet", testCaseAllocateAddress{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ - Pools: []ipamv1.Pool{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Subnet: ptr.To("192.168.0.0/30"), + }, + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.0.1": "abcd", + "192.168.0.2": "abcd", + "192.168.0.3": "abcd", + }, + expectError: true, + }), + ) + + type testK8sCaseAllocateAddress struct { + ipPool *ipamv2.IPPool + addresses map[string]string + ipAddressClaim *capipamv1.IPAddressClaim + expectedAddress string + expectedPrefix int + expectedGateway *string + expectedDNSServers []string + expectError bool + } + + DescribeTable("Test K8sAllocateAddress", + func(tc testK8sCaseAllocateAddress) { + ipPoolMgr, err := NewIPPoolManager(nil, tc.ipPool, + logr.Discard(), + ) + Expect(err).NotTo(HaveOccurred()) + allocatedAddress, prefix, gateway, err := ipPoolMgr.K8sAllocateAddress( + tc.ipAddressClaim, 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)) + }, + Entry("Empty pools", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{}, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + }, + expectError: true, + }), + Entry("One pool, pre-allocated", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + Prefix: 26, + Gateway: ptr.To("192.168.1.1"), + }, + { + Start: ptr.To("192.168.0.21"), + End: ptr.To("192.168.0.30"), + }, + }, + PreAllocations: map[string]string{ + "TestRef": "192.168.0.21", + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: "192.168.0.21", + expectedGateway: ptr.To("192.168.0.1"), + expectedPrefix: 24, + }), + Entry("One pool, pre-allocated, with overrides", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + Prefix: 26, + Gateway: ptr.To("192.168.1.1"), + }, + }, + PreAllocations: map[string]string{ + "TestRef": "192.168.0.15", + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: "192.168.0.15", + expectedGateway: ptr.To("192.168.1.1"), + expectedPrefix: 26, + }), + Entry("One pool, pre-allocated, out of bonds", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + Prefix: 26, + Gateway: ptr.To("192.168.1.1"), + }, + }, + PreAllocations: map[string]string{ + "TestRef": "192.168.0.21", + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectError: true, + }), + Entry("One pool, with start and existing address", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + }, + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.0.12": "bcde", + "192.168.0.11": "abcd", + }, + expectedAddress: "192.168.0.13", + expectedGateway: ptr.To("192.168.0.1"), + expectedPrefix: 24, + }), + Entry("One pool, with subnet and override prefix", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.11"), + End: ptr.To("192.168.0.20"), + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + Prefix: 26, + Gateway: ptr.To("192.168.1.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.0.12": "bcde", + "192.168.0.11": "abcd", + }, + expectedAddress: "192.168.0.13", + expectedGateway: ptr.To("192.168.0.1"), + expectedDNSServers: []string{ + "8.8.8.8", + }, + expectedPrefix: 24, + }), + Entry("two pools, with subnet and override prefix in first", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.10"), + Prefix: 24, + Gateway: ptr.To("192.168.1.1"), + }, + { + Subnet: ptr.To("192.168.1.10/24"), + }, + }, + Prefix: 26, + Gateway: ptr.To("192.168.2.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.1.11": "bcde", + "192.168.0.10": "abcd", + }, + expectedAddress: "192.168.1.12", + expectedGateway: ptr.To("192.168.2.1"), + expectedPrefix: 26, + }), + Entry("two pools, with subnet and override prefix", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.10"), + }, + { + Subnet: ptr.To("192.168.1.10/24"), + Prefix: 24, + Gateway: ptr.To("192.168.1.1"), + }, + }, + Prefix: 26, + Gateway: ptr.To("192.168.2.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.1.11": "bcde", + "192.168.0.10": "abcd", + }, + expectedAddress: "192.168.1.12", + expectedGateway: ptr.To("192.168.1.1"), + expectedPrefix: 24, + }), + Entry("Exhausted pools start", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ + { + Start: ptr.To("192.168.0.10"), + End: ptr.To("192.168.0.10"), + }, + }, + Prefix: 24, + Gateway: ptr.To("192.168.0.1"), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.0.10": "abcd", + }, + expectError: true, + }), + Entry("Exhausted pools subnet", testK8sCaseAllocateAddress{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + Pools: []ipamv2.Pool{ { - Subnet: (*ipamv1.IPSubnetStr)(ptr.To("192.168.0.0/30")), + Subnet: ptr.To("192.168.0.0/30"), }, }, Prefix: 24, - Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + Gateway: ptr.To("192.168.0.1"), }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, - 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", + addresses: map[string]string{ + "192.168.0.1": "abcd", + "192.168.0.2": "abcd", + "192.168.0.3": "abcd", }, expectError: true, }), ) type testCaseDeleteAddresses struct { - ipPool *ipamv1.IPPool - ipClaim *ipamv1.IPClaim - m3addresses []*ipamv1.IPAddress - addresses map[ipamv1.IPAddressStr]string - expectedAddresses map[ipamv1.IPAddressStr]string - expectedAllocations map[string]ipamv1.IPAddressStr + ipPool *ipamv2.IPPool + ipClaim *ipamv2.IPClaim + m3addresses []*ipamv2.IPAddress + addresses map[string]string + expectedAddresses map[string]string + expectedAllocations map[string]string expectError bool } @@ -1151,7 +2448,7 @@ var _ = Describe("IPPool manager", func() { } // get list of IPAddress objects - addressObjects := ipamv1.IPAddressList{} + addressObjects := ipamv2.IPAddressList{} opts := &client.ListOptions{} err = c.List(context.TODO(), &addressObjects, opts) Expect(err).NotTo(HaveOccurred()) @@ -1164,69 +2461,184 @@ var _ = Describe("IPPool manager", func() { Expect(len(tc.ipClaim.Finalizers)).To(Equal(0)) }, Entry("Empty IPPool", testCaseDeleteAddresses{ - ipPool: &ipamv1.IPPool{}, - ipClaim: &ipamv1.IPClaim{ + ipPool: &ipamv2.IPPool{}, + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, }), Entry("No Deletion needed", testCaseDeleteAddresses{ - ipPool: &ipamv1.IPPool{}, - ipClaim: &ipamv1.IPClaim{ + ipPool: &ipamv2.IPPool{}, + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, - expectedAddresses: map[ipamv1.IPAddressStr]string{ipamv1.IPAddressStr("192.168.0.1"): "abcd"}, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.1"): "abcd", + expectedAddresses: map[string]string{"192.168.0.1": "abcd"}, + addresses: map[string]string{ + "192.168.0.1": "abcd", }, }), Entry("Deletion needed, not found", testCaseDeleteAddresses{ - ipPool: &ipamv1.IPPool{ - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.1"), + ipPool: &ipamv2.IPPool{ + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "TestRef": "192.168.0.1", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipClaim: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.1"): "TestRef", + addresses: map[string]string{ + "192.168.0.1": "TestRef", }, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{}, }), Entry("Deletion needed", testCaseDeleteAddresses{ - ipPool: &ipamv1.IPPool{ - Spec: ipamv1.IPPoolSpec{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ + NamePrefix: "abc", + }, + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "TestRef": "192.168.0.1", + }, + }, + }, + ipClaim: &ipamv2.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Finalizers: []string{ + ipamv2.IPClaimFinalizer, + }, + }, + }, + addresses: map[string]string{ + "192.168.0.1": "TestRef", + }, + expectedAddresses: map[string]string{}, + expectedAllocations: map[string]string{}, + m3addresses: []*ipamv2.IPAddress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-192-168-0-1", + }, + }, + }, + }), + ) + + type testCaseK8sDeleteAddresses struct { + ipPool *ipamv2.IPPool + ipAddressClaim *capipamv1.IPAddressClaim + capiAddresses []*capipamv1.IPAddress + addresses map[string]string + expectedAddresses map[string]string + expectedAllocations map[string]string + 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: &ipamv2.IPPool{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + }), + Entry("No Deletion needed", testCaseK8sDeleteAddresses{ + ipPool: &ipamv2.IPPool{}, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddresses: map[string]string{"192.168.0.1": "abcd"}, + addresses: map[string]string{ + "192.168.0.1": "abcd", + }, + }), + Entry("Deletion needed, not found", testCaseK8sDeleteAddresses{ + ipPool: &ipamv2.IPPool{ + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "TestRef": "192.168.0.1", + }, + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + addresses: map[string]string{ + "192.168.0.1": "TestRef", + }, + expectedAllocations: map[string]string{}, + expectedAddresses: map[string]string{}, + }), + Entry("Deletion needed", testCaseK8sDeleteAddresses{ + ipPool: &ipamv2.IPPool{ + Spec: ipamv2.IPPoolSpec{ NamePrefix: "abc", }, - Status: ipamv1.IPPoolStatus{ - Allocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.1"), + Status: ipamv2.IPPoolStatus{ + Allocations: map[string]string{ + "TestRef": "192.168.0.1", }, }, }, - ipClaim: &ipamv1.IPClaim{ + ipAddressClaim: &capipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "TestRef", Finalizers: []string{ - ipamv1.IPClaimFinalizer, + IPAddressClaimFinalizer, }, }, }, - addresses: map[ipamv1.IPAddressStr]string{ - ipamv1.IPAddressStr("192.168.0.1"): "TestRef", + addresses: map[string]string{ + "192.168.0.1": "TestRef", }, - expectedAddresses: map[ipamv1.IPAddressStr]string{}, - expectedAllocations: map[string]ipamv1.IPAddressStr{}, - m3addresses: []*ipamv1.IPAddress{ + expectedAddresses: map[string]string{}, + expectedAllocations: map[string]string{}, + capiAddresses: []*capipamv1.IPAddress{ { ObjectMeta: metav1.ObjectMeta{ Name: "abc-192-168-0-1", diff --git a/ipam/manager_factory.go b/ipam/manager_factory.go index b3df617b..5e103937 100644 --- a/ipam/manager_factory.go +++ b/ipam/manager_factory.go @@ -18,12 +18,12 @@ package ipam import ( "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "sigs.k8s.io/controller-runtime/pkg/client" ) type ManagerFactoryInterface interface { - NewIPPoolManager(*ipamv1.IPPool, logr.Logger) ( + NewIPPoolManager(*ipamv2.IPPool, logr.Logger) ( IPPoolManagerInterface, error, ) } @@ -39,6 +39,6 @@ func NewManagerFactory(client client.Client) ManagerFactory { } // NewIPPoolManager creates a new IPPoolManager. -func (f ManagerFactory) NewIPPoolManager(ipPool *ipamv1.IPPool, metadataLog logr.Logger) (IPPoolManagerInterface, error) { +func (f ManagerFactory) NewIPPoolManager(ipPool *ipamv2.IPPool, metadataLog logr.Logger) (IPPoolManagerInterface, error) { return NewIPPoolManager(f.client, ipPool, metadataLog) } diff --git a/ipam/manager_factory_test.go b/ipam/manager_factory_test.go index 3a9aabd3..e1da931d 100644 --- a/ipam/manager_factory_test.go +++ b/ipam/manager_factory_test.go @@ -21,7 +21,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -41,7 +41,7 @@ var _ = Describe("Manager factory testing", func() { }) It("returns an IPPool manager", func() { - _, err := managerFactory.NewIPPoolManager(&ipamv1.IPPool{}, clusterLog) + _, err := managerFactory.NewIPPoolManager(&ipamv2.IPPool{}, clusterLog) Expect(err).NotTo(HaveOccurred()) }) diff --git a/ipam/mocks/zz_generated.manager_factory.go b/ipam/mocks/zz_generated.manager_factory.go index 153dcae5..a3cd9217 100644 --- a/ipam/mocks/zz_generated.manager_factory.go +++ b/ipam/mocks/zz_generated.manager_factory.go @@ -27,7 +27,7 @@ import ( logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" - v1alpha1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + v1alpha2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" ipam "github.com/metal3-io/ip-address-manager/ipam" ) @@ -55,7 +55,7 @@ func (m *MockManagerFactoryInterface) EXPECT() *MockManagerFactoryInterfaceMockR } // NewIPPoolManager mocks base method. -func (m *MockManagerFactoryInterface) NewIPPoolManager(arg0 *v1alpha1.IPPool, arg1 logr.Logger) (ipam.IPPoolManagerInterface, error) { +func (m *MockManagerFactoryInterface) NewIPPoolManager(arg0 *v1alpha2.IPPool, arg1 logr.Logger) (ipam.IPPoolManagerInterface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewIPPoolManager", arg0, arg1) ret0, _ := ret[0].(ipam.IPPoolManagerInterface) diff --git a/ipam/suite_test.go b/ipam/suite_test.go index 925dfac3..2f5bbbdd 100644 --- a/ipam/suite_test.go +++ b/ipam/suite_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/gomega" _ "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -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" @@ -62,13 +63,15 @@ var _ = BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, } - var err error cfg, err = testEnv.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - err = ipamv1.AddToScheme(scheme.Scheme) + err = ipamv2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = capipamv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = apiextensionsv1.AddToScheme(scheme.Scheme) @@ -100,7 +103,10 @@ var _ = AfterSuite(func() { // -----------------------------------. func setupScheme() *runtime.Scheme { s := runtime.NewScheme() - if err := ipamv1.AddToScheme(s); err != nil { + if err := ipamv2.AddToScheme(s); err != nil { + panic(err) + } + if err := capipamv1.AddToScheme(s); err != nil { panic(err) } return s diff --git a/ipam/utils_test.go b/ipam/utils_test.go index 3272e906..78fa1851 100644 --- a/ipam/utils_test.go +++ b/ipam/utils_test.go @@ -22,7 +22,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -104,8 +104,8 @@ var _ = Describe("Metal3 manager utils", func() { }) type testCaseUpdate struct { - TestObject *ipamv1.IPClaim - ExistingObject *ipamv1.IPClaim + TestObject *ipamv2.IPClaim + ExistingObject *ipamv2.IPClaim ExpectedError bool } @@ -115,7 +115,7 @@ var _ = Describe("Metal3 manager utils", func() { if tc.ExistingObject != nil { err := c.Create(context.TODO(), tc.ExistingObject) Expect(err).NotTo(HaveOccurred()) - ipPool := ipamv1.IPClaim{} + ipPool := ipamv2.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.ExistingObject.Name, @@ -135,7 +135,7 @@ var _ = Describe("Metal3 manager utils", func() { Expect(err).NotTo(HaveOccurred()) Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) Expect(obj.Status).To(Equal(tc.TestObject.Status)) - savedObject := ipamv1.IPClaim{} + savedObject := ipamv2.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.TestObject.Name, @@ -156,15 +156,15 @@ var _ = Describe("Metal3 manager utils", func() { } }, Entry("Object does not exist", testCaseUpdate{ - TestObject: &ipamv1.IPClaim{ + TestObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, @@ -172,27 +172,27 @@ var _ = Describe("Metal3 manager utils", func() { ExpectedError: true, }), Entry("Object exists", testCaseUpdate{ - TestObject: &ipamv1.IPClaim{ + TestObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, - ExistingObject: &ipamv1.IPClaim{ + ExistingObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abcd"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abcd"}, }, }, @@ -216,7 +216,7 @@ var _ = Describe("Metal3 manager utils", func() { Expect(err).NotTo(HaveOccurred()) Expect(obj.Spec).To(Equal(tc.TestObject.Spec)) Expect(obj.Status).To(Equal(tc.TestObject.Status)) - savedObject := ipamv1.IPClaim{} + savedObject := ipamv2.IPClaim{} err = c.Get(context.TODO(), client.ObjectKey{ Name: tc.TestObject.Name, @@ -233,15 +233,15 @@ var _ = Describe("Metal3 manager utils", func() { } }, Entry("Object does not exist", testCaseUpdate{ - TestObject: &ipamv1.IPClaim{ + TestObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, @@ -249,27 +249,27 @@ var _ = Describe("Metal3 manager utils", func() { ExpectedError: false, }), Entry("Object exists", testCaseUpdate{ - TestObject: &ipamv1.IPClaim{ + TestObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abc"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abc"}, }, }, - ExistingObject: &ipamv1.IPClaim{ + ExistingObject: &ipamv2.IPClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: "myns", }, - Spec: ipamv1.IPClaimSpec{ + Spec: ipamv2.IPClaimSpec{ Pool: corev1.ObjectReference{Name: "abcd"}, }, - Status: ipamv1.IPClaimStatus{ + Status: ipamv2.IPClaimStatus{ Address: &corev1.ObjectReference{Name: "abcd"}, }, }, diff --git a/main.go b/main.go index 9c5bd327..90b1c33a 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "os" "time" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" "github.com/metal3-io/ip-address-manager/controllers" "github.com/metal3-io/ip-address-manager/ipam" "github.com/spf13/pflag" @@ -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" @@ -66,8 +67,9 @@ var ( func init() { _ = scheme.AddToScheme(myscheme) - _ = ipamv1.AddToScheme(myscheme) + _ = ipamv2.AddToScheme(myscheme) _ = clusterv1.AddToScheme(myscheme) + _ = capipamv1.AddToScheme(myscheme) } // Add RBAC for the authorized diagnostics endpoint. @@ -216,24 +218,34 @@ 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) } } func setupWebhooks(mgr ctrl.Manager) { - if err := (&ipamv1.IPPool{}).SetupWebhookWithManager(mgr); err != nil { + if err := (&ipamv2.IPPool{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "IPPool") os.Exit(1) } - if err := (&ipamv1.IPAddress{}).SetupWebhookWithManager(mgr); err != nil { + if err := (&ipamv2.IPAddress{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "IPAddress") os.Exit(1) } - if err := (&ipamv1.IPClaim{}).SetupWebhookWithManager(mgr); err != nil { + if err := (&ipamv2.IPClaim{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "IPClaim") os.Exit(1) } diff --git a/test/fuzz/ippool_controller_fuzzer.go b/test/fuzz/ippool_controller_fuzzer.go index cd4dd463..086c224d 100644 --- a/test/fuzz/ippool_controller_fuzzer.go +++ b/test/fuzz/ippool_controller_fuzzer.go @@ -5,13 +5,13 @@ import ( "fmt" fuzz "github.com/AdaLogics/go-fuzz-headers" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" controller "github.com/metal3-io/ip-address-manager/controllers" "sigs.k8s.io/controller-runtime/pkg/client" ) type TestCaseM3IPCToM3IPP struct { - IPClaim *ipamv1.IPClaim + IPClaim *ipamv2.IPClaim ExpectRequest bool } diff --git a/test/fuzz/ippool_manager_fuzzer.go b/test/fuzz/ippool_manager_fuzzer.go index 583e49f7..abe13366 100644 --- a/test/fuzz/ippool_manager_fuzzer.go +++ b/test/fuzz/ippool_manager_fuzzer.go @@ -5,13 +5,13 @@ import ( fuzz "github.com/AdaLogics/go-fuzz-headers" "github.com/go-logr/logr" - ipamv1 "github.com/metal3-io/ip-address-manager/api/v1alpha1" + ipamv2 "github.com/metal3-io/ip-address-manager/api/v1alpha2" ipam "github.com/metal3-io/ip-address-manager/ipam" ) func FuzzNewIPPoolManager(data []byte) int { f := fuzz.NewConsumer(data) - ipPool := &ipamv1.IPPool{} + ipPool := &ipamv2.IPPool{} err := f.GenerateStruct(ipPool) if err != nil {