diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml
index f797a7d56f..b82bcc11f4 100644
--- a/bootstrapper-maven-plugin/pom.xml
+++ b/bootstrapper-maven-plugin/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
bootstrapper
diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml
index fb5e2c98a1..a63c5889fe 100644
--- a/caffeine-bounded-cache-support/pom.xml
+++ b/caffeine-bounded-cache-support/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
4.0.0
diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml
index 3e640e3e7f..6f6669999a 100644
--- a/micrometer-support/pom.xml
+++ b/micrometer-support/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
4.0.0
diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml
index e54776b161..75ead60afa 100644
--- a/operator-framework-bom/pom.xml
+++ b/operator-framework-bom/pom.xml
@@ -5,7 +5,7 @@
io.javaoperatorsdk
operator-framework-bom
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
Operator SDK - Bill of Materials
pom
Java SDK for implementing Kubernetes operators
diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml
index b4afcf0d33..dc054d587a 100644
--- a/operator-framework-core/pom.xml
+++ b/operator-framework-core/pom.xml
@@ -6,7 +6,7 @@
io.javaoperatorsdk
java-operator-sdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
../pom.xml
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleBindingReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleBindingReconciler.java
new file mode 100644
index 0000000000..eda579d80a
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleBindingReconciler.java
@@ -0,0 +1,22 @@
+package io.javaoperatorsdk.operator.support;
+
+import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
+import io.javaoperatorsdk.operator.api.reconciler.*;
+
+// todo handle also ClusterRole bindings only if has permission
+@ControllerConfiguration
+public class NamespaceDeletionRoleBindingReconciler
+ implements Reconciler, Cleaner {
+
+ @Override
+ public UpdateControl reconcile(RoleBinding resource, Context context)
+ throws Exception {
+
+ return UpdateControl.noUpdate();
+ }
+
+ @Override
+ public DeleteControl cleanup(RoleBinding resource, Context context) {
+ return null;
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleReconciler.java
new file mode 100644
index 0000000000..1ef4344d1f
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NamespaceDeletionRoleReconciler.java
@@ -0,0 +1,112 @@
+package io.javaoperatorsdk.operator.support;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.rbac.Role;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.*;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+
+// todo label selector needs to added explicitly
+@ControllerConfiguration(onAddFilter = NonMarkedForDeletionAddFilter.class,
+ onUpdateFilter = NonMarkedForDeletionUpdateFilter.class)
+public class NamespaceDeletionRoleReconciler
+ implements Reconciler, Cleaner, EventSourceInitializer {
+
+ public static final String TARGET_RESOURCES_IN_NAMESPACE_TO_ROLE_INDEX =
+ "target-resources-in-namespace";
+ public static final String RESOURCE_NAMESPACE_INDEX = "resource-namespace-index";
+
+ final List> resourceClasses;
+ final Set resourcePlurals;
+ final Map, String> classToPlural;
+ final Map> pluralToClass;
+
+ public NamespaceDeletionRoleReconciler(List> resourceClasses) {
+ this.resourceClasses = resourceClasses;
+ this.classToPlural =
+ resourceClasses.stream().collect(Collectors.toMap(c -> c, HasMetadata::getPlural));
+ this.pluralToClass =
+ resourceClasses.stream().collect(Collectors.toMap(HasMetadata::getPlural, c -> c));
+ this.resourcePlurals =
+ resourceClasses.stream().map(HasMetadata::getPlural).collect(Collectors.toSet());
+ }
+
+ @Override
+ public UpdateControl reconcile(Role resource, Context context) throws Exception {
+ return UpdateControl.noUpdate();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public DeleteControl cleanup(Role resource, Context context) {
+ AtomicBoolean watchedResourceExistsInNamespace = new AtomicBoolean(false);
+ resource.getRules().forEach(rule -> {
+ rule.getResources().forEach(r -> {
+ if (resourcePlurals.contains(r)) {
+ InformerEventSource es =
+ (InformerEventSource) context.eventSourceRetriever()
+ .getResourceEventSourceFor(pluralToClass.get(r));
+ var resources =
+ es.byIndex(RESOURCE_NAMESPACE_INDEX, resource.getMetadata().getNamespace());
+ if (!resources.isEmpty()) {
+ watchedResourceExistsInNamespace.set(true);
+ }
+ }
+ });
+ });
+ if (watchedResourceExistsInNamespace.get()) {
+ return DeleteControl.noFinalizerRemoval();
+ } else {
+ return DeleteControl.defaultDelete();
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public Map prepareEventSources(EventSourceContext context) {
+ var allPlurals = classToPlural.values();
+ context.getPrimaryCache().addIndexer(TARGET_RESOURCES_IN_NAMESPACE_TO_ROLE_INDEX, r -> {
+ // resource-plural+namespace -> role (in that namespace)
+ List result = new ArrayList<>();
+ r.getRules().forEach(rule -> rule.getResources().forEach(resource -> {
+ if (allPlurals.contains(resource)) {
+ result.add(keyFor(r.getMetadata().getNamespace(), resource));
+ }
+ }));
+ return result;
+ });
+
+ return resourceClasses.stream()
+ .map(c -> {
+ var ies =
+ new InformerEventSource(InformerConfiguration.from(c, context)
+ .withSecondaryToPrimaryMapper((SecondaryToPrimaryMapper) resource -> {
+ HasMetadata rm = (HasMetadata) resource;
+ var roles = context.getPrimaryCache().byIndex(
+ TARGET_RESOURCES_IN_NAMESPACE_TO_ROLE_INDEX,
+ keyFor(rm.getMetadata().getNamespace(), rm.getPlural()));
+ return roles.stream().map(r -> new ResourceID(r.getMetadata().getName(),
+ r.getMetadata().getNamespace())).collect(Collectors.toSet());
+ })
+ .build(), context);
+ ies.addIndexer(RESOURCE_NAMESPACE_INDEX, r -> List.of(r.getMetadata().getNamespace()));
+ return ies;
+ })
+ .collect(Collectors.toMap(i -> classToPlural.get(i.resourceType()), i -> i));
+ }
+
+ public static String keyFor(String namespace, String resourcePlural) {
+ return resourcePlural + "-" + namespace;
+ }
+
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionAddFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionAddFilter.java
new file mode 100644
index 0000000000..c409e0d803
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionAddFilter.java
@@ -0,0 +1,12 @@
+package io.javaoperatorsdk.operator.support;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
+
+public class NonMarkedForDeletionAddFilter implements OnAddFilter {
+ @Override
+ public boolean accept(T resource) {
+ return resource.getMetadata().getDeletionTimestamp() != null;
+ }
+
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionUpdateFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionUpdateFilter.java
new file mode 100644
index 0000000000..0bb9cf83ea
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/support/NonMarkedForDeletionUpdateFilter.java
@@ -0,0 +1,12 @@
+package io.javaoperatorsdk.operator.support;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
+
+public class NonMarkedForDeletionUpdateFilter implements OnUpdateFilter {
+
+ @Override
+ public boolean accept(T newResource, T oldResource) {
+ return newResource.getMetadata().getDeletionTimestamp() != null;
+ }
+}
diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml
index a80d03d69e..9366485f25 100644
--- a/operator-framework-junit5/pom.xml
+++ b/operator-framework-junit5/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
4.0.0
diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml
index ff38d5fc66..430cc508b2 100644
--- a/operator-framework/pom.xml
+++ b/operator-framework/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
4.0.0
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/NamespaceDeletionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/NamespaceDeletionIT.java
new file mode 100644
index 0000000000..44d8b372d0
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/NamespaceDeletionIT.java
@@ -0,0 +1,112 @@
+package io.javaoperatorsdk.operator;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import io.fabric8.kubernetes.api.model.Namespace;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.rbac.Role;
+import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.sample.namespacedeletion.NamespaceDeletionTestCustomResource;
+import io.javaoperatorsdk.operator.sample.namespacedeletion.NamespaceDeletionTestReconciler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public class NamespaceDeletionIT {
+
+ KubernetesClient adminClient = new KubernetesClientBuilder().build();
+
+ KubernetesClient client = new KubernetesClientBuilder()
+ .withConfig(new ConfigBuilder()
+ .withImpersonateUsername("namespace-deletion-test-user")
+ .build())
+ .build();
+
+ String actualNamespace;
+ Operator operator;
+
+ @BeforeEach
+ void beforeEach(TestInfo testInfo) {
+ LocallyRunOperatorExtension.applyCrd(NamespaceDeletionTestCustomResource.class,
+ adminClient);
+
+ testInfo.getTestMethod().ifPresent(method -> {
+ actualNamespace = KubernetesResourceUtil.sanitizeName(method.getName());
+ adminClient.resource(namespace()).create();
+ });
+
+ applyRBACResources();
+ operator = new Operator(client);
+ operator.register(new NamespaceDeletionTestReconciler(),
+ o -> o.settingNamespaces(actualNamespace));
+ operator.start();
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (operator != null) {
+ operator.stop(Duration.ofSeconds(1));
+ }
+ }
+
+ @Test
+ void testDeletingNamespaceWithRolesForOperator() {
+ var res = adminClient.resource(testResource()).create();
+
+ await().untilAsserted(() -> {
+ var actual = adminClient.resource(res).get();
+ assertThat(actual.getMetadata().getFinalizers()).isNotEmpty();
+ });
+
+ adminClient.resource(namespace()).delete();
+
+ await().untilAsserted(() -> {
+ var actual = adminClient.resource(res).get();
+ assertThat(actual).isNull();
+ });
+ }
+
+ NamespaceDeletionTestCustomResource testResource() {
+ NamespaceDeletionTestCustomResource resource = new NamespaceDeletionTestCustomResource();
+ resource.setMetadata(new ObjectMetaBuilder()
+ .withName("test1")
+ .withNamespace(actualNamespace)
+ .build());
+ return resource;
+ }
+
+ private Namespace namespace() {
+ return namespace(actualNamespace);
+ }
+
+ private Namespace namespace(String name) {
+ Namespace n = new Namespace();
+ n.setMetadata(new ObjectMetaBuilder()
+ .withName(name)
+ .withName(actualNamespace)
+ .build());
+ return n;
+ }
+
+ private void applyRBACResources() {
+ var role = ReconcilerUtils
+ .loadYaml(Role.class, NamespaceDeletionTestReconciler.class, "role.yaml");
+ role.getMetadata().setNamespace(actualNamespace);
+ adminClient.resource(role).create();
+
+ var roleBinding = ReconcilerUtils
+ .loadYaml(RoleBinding.class, NamespaceDeletionTestReconciler.class, "role-binding.yaml");
+ roleBinding.getMetadata().setNamespace(actualNamespace);
+ adminClient.resource(roleBinding).create();
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestCustomResource.java
new file mode 100644
index 0000000000..06a0758b0b
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestCustomResource.java
@@ -0,0 +1,15 @@
+package io.javaoperatorsdk.operator.sample.namespacedeletion;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("sample.javaoperatorsdk")
+@Version("v1")
+@ShortNames("ndt")
+public class NamespaceDeletionTestCustomResource
+ extends CustomResource
+ implements Namespaced {
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestReconciler.java
new file mode 100644
index 0000000000..1682321b4d
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/namespacedeletion/NamespaceDeletionTestReconciler.java
@@ -0,0 +1,38 @@
+package io.javaoperatorsdk.operator.sample.namespacedeletion;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.javaoperatorsdk.operator.api.reconciler.*;
+import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
+
+@ControllerConfiguration
+public class NamespaceDeletionTestReconciler
+ implements Reconciler, TestExecutionInfoProvider,
+ Cleaner {
+
+ public static final int CLEANER_WAIT_PERIOD = 300;
+ private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
+
+ @Override
+ public UpdateControl reconcile(
+ NamespaceDeletionTestCustomResource resource,
+ Context context) {
+ numberOfExecutions.addAndGet(1);
+ return UpdateControl.noUpdate();
+ }
+
+ public int getNumberOfExecutions() {
+ return numberOfExecutions.get();
+ }
+
+ @Override
+ public DeleteControl cleanup(NamespaceDeletionTestCustomResource resource,
+ Context context) {
+ try {
+ Thread.sleep(CLEANER_WAIT_PERIOD);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ return DeleteControl.defaultDelete();
+ }
+}
diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role-binding.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role-binding.yaml
new file mode 100644
index 0000000000..78ae143932
--- /dev/null
+++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role-binding.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
+kind: RoleBinding
+metadata:
+ name: namespace-deletion
+subjects:
+ - kind: User
+ name: namespace-deletion-test-user
+ apiGroup: rbac.authorization.k8s.io
+roleRef:
+ kind: Role
+ name: namespace-deletion-test
+ apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role.yaml
new file mode 100644
index 0000000000..de6a9dd036
--- /dev/null
+++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/namespacedeletion/role.yaml
@@ -0,0 +1,11 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ # "namespace" omitted since ClusterRoles are not namespaced
+ name: namespace-deletion-test
+rules:
+ - apiGroups: [ "sample.javaoperatorsdk" ]
+ resources: [ "namespacedeletiontestcustomresources" ]
+ verbs: [ "get", "watch", "list", "post", "update", "patch", "delete" ]
+
+
diff --git a/pom.xml b/pom.xml
index 6ca4f9684f..c389c01c2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
io.javaoperatorsdk
java-operator-sdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
Operator SDK for Java
Java SDK for implementing Kubernetes operators
pom
diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml
index af8589be61..ea18538d30 100644
--- a/sample-operators/leader-election/pom.xml
+++ b/sample-operators/leader-election/pom.xml
@@ -7,7 +7,7 @@
io.javaoperatorsdk
sample-operators
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
sample-leader-election
diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml
index c69c388299..7dba03f3d9 100644
--- a/sample-operators/mysql-schema/pom.xml
+++ b/sample-operators/mysql-schema/pom.xml
@@ -7,7 +7,7 @@
io.javaoperatorsdk
sample-operators
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
sample-mysql-schema-operator
diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml
index f7b1b4dfcb..56c7834d03 100644
--- a/sample-operators/pom.xml
+++ b/sample-operators/pom.xml
@@ -7,7 +7,7 @@
io.javaoperatorsdk
java-operator-sdk
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
sample-operators
diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml
index ffb17e2cda..89e243ba0a 100644
--- a/sample-operators/tomcat-operator/pom.xml
+++ b/sample-operators/tomcat-operator/pom.xml
@@ -7,7 +7,7 @@
io.javaoperatorsdk
sample-operators
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
sample-tomcat-operator
diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml
index 8cbac1f178..e0becee1a5 100644
--- a/sample-operators/webpage/pom.xml
+++ b/sample-operators/webpage/pom.xml
@@ -7,7 +7,7 @@
io.javaoperatorsdk
sample-operators
- 4.6.1-SNAPSHOT
+ 4.7.0-SNAPSHOT
sample-webpage-operator