From 1ceca2e5d589c0795241ebf8b903c7d9fdc5e7d8 Mon Sep 17 00:00:00 2001 From: Cyrill Berg Date: Sat, 27 Jun 2026 01:50:04 +0200 Subject: [PATCH] fix(apibinding): release CRD resource lock when the CRD is deleted The logicalclustercleanup controller clears a CRD locks expiry while the CRD exists, but the removal branch only fired for locks with a non-nil, passed expiry. So once an established CRD was deleted, its resource-bindings lock was never released, blocking new APIBindings for the same group-resource. Release the lock when its CRD no longer exists and it has no expiry set. Fixes #3799 Signed-off-by: Cyrill Berg --- .../logicalclustercleanup_controller.go | 8 +++++--- .../logicalclustercleanup_controller_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller.go b/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller.go index 8f6f6f7b463..9737c948e28 100644 --- a/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller.go +++ b/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller.go @@ -308,9 +308,11 @@ func (c *controller) process(ctx context.Context, key string) error { continue } - // CRD doesn't exist. - if b.CRDExpiry != nil && time.Now().After(b.CRDExpiry.Time) { - logger.V(4).Info("removing expired CRD binding of non-existing CRD", "crd", gr) + // CRD doesn't exist (anymore): release the lock immediately if it was + // held without an expiry (the CRD was deleted), or once an existing + // expiry has passed. + if b.CRDExpiry == nil || time.Now().After(b.CRDExpiry.Time) { + logger.V(4).Info("removing CRD binding of non-existing CRD", "crd", gr) delete(rbs, gr) } } diff --git a/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller_test.go b/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller_test.go index 73cea8f90d4..10471a06c5b 100644 --- a/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller_test.go +++ b/pkg/reconciler/apis/logicalclustercleanup/logicalclustercleanup_controller_test.go @@ -97,6 +97,10 @@ func TestReconciler(t *testing.T) { logicalCluster: &corev1alpha1.LogicalCluster{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ "internal.apis.kcp.io/resource-bindings": `{"as.group":{"n":"binding1"},"bs.group":{"n":"binding1"},"crd1s.group":{"c":true},"crd2s.group":{"c":true},"cs.group":{"n":"binding2"},"ds.group":{"n":"binding2"}}`, }}}, + crds: []*apiextensionsv1.CustomResourceDefinition{ + withEstablished(newCRD("group", "crd1s")), + withEstablished(newCRD("group", "crd2s")), + }, apiBindings: []*apisv1alpha2.APIBinding{ &newAPIBinding().WithName("binding1").WithBoundResources("group", "as", "group", "bs").APIBinding, }, @@ -115,6 +119,15 @@ func TestReconciler(t *testing.T) { "internal.apis.kcp.io/resource-bindings": fmt.Sprintf(`{"crd1s.group":{"c":true},"crd3s.group":{"c":true,"e":%q}}`, notExpired), }}}, }, + "deleted CRD whose lock has no expiry is removed": { + logicalCluster: &corev1alpha1.LogicalCluster{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + "internal.apis.kcp.io/resource-bindings": `{"crd1s.group":{"c":true}}`, + }}}, + crds: []*apiextensionsv1.CustomResourceDefinition{}, + want: &corev1alpha1.LogicalCluster{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + "internal.apis.kcp.io/resource-bindings": `{}`, + }}}, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) {