2828import java .util .HashSet ;
2929import java .util .List ;
3030import java .util .Map ;
31+ import java .util .Objects ;
3132import java .util .Set ;
33+ import java .util .concurrent .TimeUnit ;
3234import java .util .function .Consumer ;
3335import java .util .function .Function ;
3436import java .util .stream .Stream ;
4244import io .fabric8 .kubernetes .api .model .apiextensions .v1 .CustomResourceDefinition ;
4345import io .fabric8 .kubernetes .client .CustomResource ;
4446import io .fabric8 .kubernetes .client .KubernetesClient ;
47+ import io .fabric8 .kubernetes .client .KubernetesClientBuilder ;
4548import io .fabric8 .kubernetes .client .LocalPortForward ;
4649import io .javaoperatorsdk .operator .Operator ;
4750import io .javaoperatorsdk .operator .ReconcilerUtilsInternal ;
@@ -58,6 +61,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension {
5861
5962 private static final Logger LOGGER = LoggerFactory .getLogger (LocallyRunOperatorExtension .class );
6063 private static final int CRD_DELETE_TIMEOUT = 5000 ;
64+ private static final int CRD_DELETE_WAIT_TIMEOUT = 60000 ;
6165 private static final Set <AppliedCRD > appliedCRDs = new HashSet <>();
6266 private static final boolean deleteCRDs =
6367 Boolean .parseBoolean (System .getProperty ("testsuite.deleteCRDs" , "true" ));
@@ -349,6 +353,13 @@ protected void before(ExtensionContext context) {
349353 beforeStartHook .accept (this );
350354 }
351355
356+ // ExtensionContext.Store.CloseableResource registered in the class-level (parent) context
357+ // is invoked by JUnit when the class scope closes
358+ var classContext = oneNamespacePerClass ? context : context .getParent ().orElse (context );
359+ classContext
360+ .getStore (ExtensionContext .Namespace .create (LocallyRunOperatorExtension .class ))
361+ .computeIfAbsent (CrdCleanup .class , ignored -> new CrdCleanup ());
362+
352363 LOGGER .debug ("Starting the operator locally" );
353364 this .operator .start ();
354365 }
@@ -357,18 +368,12 @@ protected void before(ExtensionContext context) {
357368 protected void after (ExtensionContext context ) {
358369 super .after (context );
359370
360- var kubernetesClient = getInfrastructureKubernetesClient ();
361-
362- var iterator = appliedCRDs .iterator ();
363- while (iterator .hasNext ()) {
364- deleteCrd (iterator .next (), kubernetesClient );
365- iterator .remove ();
366- }
371+ var infrastructureKubernetesClient = getInfrastructureKubernetesClient ();
367372
368373 // if the client is used for infra client, we should not close it
369374 // either test or operator should close this client
370- if (getKubernetesClient () != getInfrastructureKubernetesClient () ) {
371- kubernetesClient .close ();
375+ if (getKubernetesClient () != infrastructureKubernetesClient ) {
376+ infrastructureKubernetesClient .close ();
372377 }
373378
374379 try {
@@ -387,12 +392,27 @@ protected void after(ExtensionContext context) {
387392 localPortForwards .clear ();
388393 }
389394
390- private void deleteCrd (AppliedCRD appliedCRD , KubernetesClient client ) {
391- if (!deleteCRDs ) {
392- LOGGER .debug ("Skipping deleting CRD because of configuration: {}" , appliedCRD );
393- return ;
395+ private static class CrdCleanup implements ExtensionContext .Store .CloseableResource {
396+ @ Override
397+ public void close () {
398+ // Create a fresh client for cleanup since operator clients may already be closed.
399+ try (var client = new KubernetesClientBuilder ().build ()) {
400+ var iterator = appliedCRDs .iterator ();
401+ while (iterator .hasNext ()) {
402+ var appliedCRD = iterator .next ();
403+ iterator .remove ();
404+ if (!deleteCRDs ) {
405+ LOGGER .debug ("Skipping deleting CRD because of configuration: {}" , appliedCRD );
406+ continue ;
407+ }
408+ try {
409+ appliedCRD .delete (client );
410+ } catch (Exception e ) {
411+ LOGGER .warn ("Failed to delete CRD: {}. Continuing with remaining CRDs." , appliedCRD , e );
412+ }
413+ }
414+ }
394415 }
395- appliedCRD .delete (client );
396416 }
397417
398418 private sealed interface AppliedCRD permits AppliedCRD .FileCRD , AppliedCRD .InstanceCRD {
@@ -409,8 +429,25 @@ record FileCRD(String crdString, String path) implements AppliedCRD {
409429 public void delete (KubernetesClient client ) {
410430 try {
411431 LOGGER .debug ("Deleting CRD: {}" , crdString );
412- final var crd = client .load (new ByteArrayInputStream (crdString .getBytes ()));
413- crd .withTimeoutInMillis (CRD_DELETE_TIMEOUT ).delete ();
432+ final var items = client .load (new ByteArrayInputStream (crdString .getBytes ())).items ();
433+ if (items == null || items .isEmpty () || items .get (0 ) == null ) {
434+ LOGGER .warn ("Could not determine CRD name from yaml: {}" , path );
435+ return ;
436+ }
437+ final var crdName = items .get (0 ).getMetadata ().getName ();
438+ client
439+ .apiextensions ()
440+ .v1 ()
441+ .customResourceDefinitions ()
442+ .withName (crdName )
443+ .withTimeoutInMillis (CRD_DELETE_TIMEOUT )
444+ .delete ();
445+ client
446+ .apiextensions ()
447+ .v1 ()
448+ .customResourceDefinitions ()
449+ .withName (crdName )
450+ .waitUntilCondition (Objects ::isNull , CRD_DELETE_WAIT_TIMEOUT , TimeUnit .MILLISECONDS );
414451 LOGGER .debug ("Deleted CRD with path: {}" , path );
415452 } catch (Exception ex ) {
416453 LOGGER .warn (
@@ -423,17 +460,28 @@ record InstanceCRD(CustomResourceDefinition customResourceDefinition) implements
423460
424461 @ Override
425462 public void delete (KubernetesClient client ) {
426- String type = customResourceDefinition .getMetadata ().getName ();
463+ String crdName = customResourceDefinition .getMetadata ().getName ();
427464 try {
428- LOGGER .debug ("Deleting CustomResourceDefinition instance CRD: {}" , type );
429- final var crd = client .resource (customResourceDefinition );
430- crd .withTimeoutInMillis (CRD_DELETE_TIMEOUT ).delete ();
431- LOGGER .debug ("Deleted CustomResourceDefinition instance CRD: {}" , type );
465+ LOGGER .debug ("Deleting CustomResourceDefinition instance CRD: {}" , crdName );
466+ client
467+ .apiextensions ()
468+ .v1 ()
469+ .customResourceDefinitions ()
470+ .withName (crdName )
471+ .withTimeoutInMillis (CRD_DELETE_TIMEOUT )
472+ .delete ();
473+ client
474+ .apiextensions ()
475+ .v1 ()
476+ .customResourceDefinitions ()
477+ .withName (crdName )
478+ .waitUntilCondition (Objects ::isNull , CRD_DELETE_WAIT_TIMEOUT , TimeUnit .MILLISECONDS );
479+ LOGGER .debug ("Deleted CustomResourceDefinition instance CRD: {}" , crdName );
432480 } catch (Exception ex ) {
433481 LOGGER .warn (
434482 "Cannot delete CustomResourceDefinition instance CRD: {}. You might need to delete it"
435483 + " manually." ,
436- type ,
484+ crdName ,
437485 ex );
438486 }
439487 }
0 commit comments