diff --git a/internal/webhooks/inclusterippool.go b/internal/webhooks/inclusterippool.go index 86342c9..e21d88e 100644 --- a/internal/webhooks/inclusterippool.go +++ b/internal/webhooks/inclusterippool.go @@ -20,6 +20,13 @@ import ( "github.com/telekom/cluster-api-ipam-provider-in-cluster/pkg/types" ) +const ( + // SkipValidateDeleteWebhookAnnotation is an annotation that can be applied + // to the InClusterIPPool or GlobalInClusterIPPool to skip delete + // validation. Necessary for clusterctl move to work as expected. + SkipValidateDeleteWebhookAnnotation = "ipam.cluster.x-k8s.io/skip-validate-delete-webhook" +) + func (webhook *InClusterIPPool) SetupWebhookWithManager(mgr ctrl.Manager) error { err := ctrl.NewWebhookManagedBy(mgr). For(&v1alpha1.InClusterIPPool{}). @@ -129,6 +136,10 @@ func (webhook *InClusterIPPool) ValidateDelete(ctx context.Context, obj runtime. return apierrors.NewBadRequest(fmt.Sprintf("expected a InClusterIPPool or an GlobalInClusterIPPool but got a %T", obj)) } + if _, ok := pool.GetAnnotations()[SkipValidateDeleteWebhookAnnotation]; ok { + return nil + } + poolTypeRef := corev1.TypedLocalObjectReference{ APIGroup: pointer.String(pool.GetObjectKind().GroupVersionKind().Group), Kind: pool.GetObjectKind().GroupVersionKind().Kind, diff --git a/internal/webhooks/inclusterippool_test.go b/internal/webhooks/inclusterippool_test.go index d9c0639..68d5ade 100644 --- a/internal/webhooks/inclusterippool_test.go +++ b/internal/webhooks/inclusterippool_test.go @@ -100,6 +100,89 @@ func TestPoolDeletionWithExistingIPAddresses(t *testing.T) { g.Expect(webhook.ValidateDelete(ctx, globalPool)).To(Succeed(), "should allow deletion when no claims exist") } +func TestDeleteSkip(t *testing.T) { + g := NewWithT(t) + + scheme := runtime.NewScheme() + g.Expect(ipamv1.AddToScheme(scheme)).To(Succeed()) + + namespacedPool := &v1alpha1.InClusterIPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pool", + Annotations: map[string]string{ + SkipValidateDeleteWebhookAnnotation: "", + }, + }, + Spec: v1alpha1.InClusterIPPoolSpec{ + Addresses: []string{"10.0.0.10-10.0.0.20"}, + Prefix: 24, + Gateway: "10.0.0.1", + }, + } + + globalPool := &v1alpha1.InClusterIPPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pool", + Annotations: map[string]string{ + SkipValidateDeleteWebhookAnnotation: "", + }, + }, + Spec: v1alpha1.InClusterIPPoolSpec{ + Addresses: []string{"10.0.0.10-10.0.0.20"}, + Prefix: 24, + Gateway: "10.0.0.1", + }, + } + + ips := []client.Object{ + &ipamv1.IPAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: "IPAddress", + APIVersion: "ipam.cluster.x-k8s.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-ip", + }, + Spec: ipamv1.IPAddressSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(namespacedPool.GetObjectKind().GroupVersionKind().Group), + Kind: namespacedPool.GetObjectKind().GroupVersionKind().Kind, + Name: namespacedPool.GetName(), + }, + }, + }, + &ipamv1.IPAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: "IPAddress", + APIVersion: "ipam.cluster.x-k8s.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-ip-2", + }, + Spec: ipamv1.IPAddressSpec{ + PoolRef: corev1.TypedLocalObjectReference{ + APIGroup: pointer.String(globalPool.GetObjectKind().GroupVersionKind().Group), + Kind: globalPool.GetObjectKind().GroupVersionKind().Kind, + Name: globalPool.GetName(), + }, + }, + }, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ips...). + WithIndex(&ipamv1.IPAddress{}, index.IPAddressPoolRefCombinedField, index.IPAddressByCombinedPoolRef). + Build() + + webhook := InClusterIPPool{ + Client: fakeClient, + } + + g.Expect(webhook.ValidateDelete(ctx, namespacedPool)).To(Succeed()) + g.Expect(webhook.ValidateDelete(ctx, globalPool)).To(Succeed()) +} + func TestInClusterIPPoolDefaulting(t *testing.T) { g := NewWithT(t)