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