@@ -476,5 +476,86 @@ var _ = Describe("CRD", func() {
476476
477477 Expect (env .Client .Delete (ctx , rgd )).To (Succeed ())
478478 })
479+
480+ It ("should detect externally deleted CRD and recreate it" , func (ctx SpecContext ) {
481+ rgdName := "test-crd-ext-deletion"
482+ rgd := generator .NewResourceGraphDefinition (rgdName ,
483+ generator .WithSchema (
484+ "TestExtDeletion" , "v1alpha1" ,
485+ map [string ]interface {}{
486+ "field1" : "string" ,
487+ "field2" : "integer | default=42" ,
488+ },
489+ nil ,
490+ ),
491+ generator .WithResource ("res1" , map [string ]interface {}{
492+ "apiVersion" : "v1" ,
493+ "kind" : "ConfigMap" ,
494+ "metadata" : map [string ]interface {}{
495+ "name" : "${schema.spec.field1}" ,
496+ },
497+ "data" : map [string ]interface {}{
498+ "key" : "value" ,
499+ "key2" : "${string(schema.spec.field2)}" ,
500+ },
501+ }, nil , nil ),
502+ )
503+
504+ Expect (env .Client .Create (ctx , rgd )).To (Succeed ())
505+
506+ // Wait for RGD to become Active and CRD to be created
507+ crdName := "testextdeletions.kro.run"
508+ crd := & apiextensionsv1.CustomResourceDefinition {}
509+ Eventually (func (g Gomega , ctx SpecContext ) {
510+ err := env .Client .Get (ctx , types.NamespacedName {Name : rgd .Name }, rgd )
511+ g .Expect (err ).ToNot (HaveOccurred ())
512+ g .Expect (rgd .Status .State ).To (Equal (krov1alpha1 .ResourceGraphDefinitionStateActive ))
513+
514+ err = env .Client .Get (ctx , types.NamespacedName {Name : crdName }, crd )
515+ g .Expect (err ).ToNot (HaveOccurred ())
516+ g .Expect (metadata .IsKROOwned (& crd .ObjectMeta )).To (BeTrue ())
517+ g .Expect (crd .Labels [metadata .ResourceGraphDefinitionNameLabel ]).To (Equal (rgdName ))
518+ }, 10 * time .Second , time .Second ).WithContext (ctx ).Should (Succeed ())
519+
520+ // Record the original CRD UID so we can verify it gets recreated
521+ // (not just the old one surviving deletion).
522+ originalUID := crd .UID
523+
524+ // Externally delete the managed CRD to simulate an out-of-band deletion.
525+ Expect (env .Client .Delete (ctx , crd )).To (Succeed ())
526+
527+ // The controller's CRD metadata watch should detect the deletion event and
528+ // trigger a reconciliation of the owning RGD, which calls crdManager.Ensure()
529+ // to recreate the CRD.
530+ //
531+ // Without the fix (DeleteFunc returning false in the CRD watch predicate),
532+ // this would timeout because the delete event would be silently filtered
533+ // out, leaving the CRD permanently deleted.
534+ recreatedCRD := & apiextensionsv1.CustomResourceDefinition {}
535+ Eventually (func (g Gomega , ctx SpecContext ) {
536+ err := env .Client .Get (ctx , types.NamespacedName {Name : crdName }, recreatedCRD )
537+ g .Expect (err ).ToNot (HaveOccurred ())
538+
539+ // A different UID proves this is a new object, not the original
540+ g .Expect (recreatedCRD .UID ).NotTo (Equal (originalUID ))
541+
542+ g .Expect (metadata .IsKROOwned (& recreatedCRD .ObjectMeta )).To (BeTrue ())
543+ g .Expect (recreatedCRD .Labels [metadata .ResourceGraphDefinitionNameLabel ]).To (Equal (rgdName ))
544+
545+ schemaProps := recreatedCRD .Spec .Versions [0 ].Schema .OpenAPIV3Schema .Properties ["spec" ].Properties
546+ g .Expect (schemaProps ["field1" ].Type ).To (Equal ("string" ))
547+ g .Expect (schemaProps ["field2" ].Type ).To (Equal ("integer" ))
548+ g .Expect (schemaProps ["field2" ].Default .Raw ).To (Equal ([]byte ("42" )))
549+ }, 30 * time .Second , 2 * time .Second ).WithContext (ctx ).Should (Succeed ())
550+
551+ // Verify the RGD recovers to Active state after CRD recreation
552+ Eventually (func (g Gomega , ctx SpecContext ) {
553+ err := env .Client .Get (ctx , types.NamespacedName {Name : rgd .Name }, rgd )
554+ g .Expect (err ).ToNot (HaveOccurred ())
555+ g .Expect (rgd .Status .State ).To (Equal (krov1alpha1 .ResourceGraphDefinitionStateActive ))
556+ }, 10 * time .Second , time .Second ).WithContext (ctx ).Should (Succeed ())
557+
558+ Expect (env .Client .Delete (ctx , rgd )).To (Succeed ())
559+ })
479560 })
480561})
0 commit comments