Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: namespace role/rolebinding deletion problem #1890

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bootstrapper-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>java-operator-sdk</artifactId>
<groupId>io.javaoperatorsdk</groupId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
</parent>

<artifactId>bootstrapper</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion caffeine-bounded-cache-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>java-operator-sdk</artifactId>
<groupId>io.javaoperatorsdk</groupId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion micrometer-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>java-operator-sdk</artifactId>
<groupId>io.javaoperatorsdk</groupId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion operator-framework-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-bom</artifactId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
<name>Operator SDK - Bill of Materials</name>
<packaging>pom</packaging>
<description>Java SDK for implementing Kubernetes operators</description>
Expand Down
2 changes: 1 addition & 1 deletion operator-framework-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<RoleBinding>, Cleaner<RoleBinding> {

@Override
public UpdateControl<RoleBinding> reconcile(RoleBinding resource, Context<RoleBinding> context)
throws Exception {

return UpdateControl.noUpdate();
}

@Override
public DeleteControl cleanup(RoleBinding resource, Context<RoleBinding> context) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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<Role>, Cleaner<Role>, EventSourceInitializer<Role> {

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<Class<? extends HasMetadata>> resourceClasses;
final Set<String> resourcePlurals;
final Map<Class<? extends HasMetadata>, String> classToPlural;
final Map<String, Class<? extends HasMetadata>> pluralToClass;

public NamespaceDeletionRoleReconciler(List<Class<? extends HasMetadata>> 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<Role> reconcile(Role resource, Context<Role> context) throws Exception {
return UpdateControl.noUpdate();
}

@SuppressWarnings("unchecked")
@Override
public DeleteControl cleanup(Role resource, Context<Role> context) {
AtomicBoolean watchedResourceExistsInNamespace = new AtomicBoolean(false);
resource.getRules().forEach(rule -> {
rule.getResources().forEach(r -> {
if (resourcePlurals.contains(r)) {
InformerEventSource<HasMetadata, Role> es =
(InformerEventSource<HasMetadata, Role>) 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<String, EventSource> prepareEventSources(EventSourceContext<Role> context) {
var allPlurals = classToPlural.values();
context.getPrimaryCache().addIndexer(TARGET_RESOURCES_IN_NAMESPACE_TO_ROLE_INDEX, r -> {
// resource-plural+namespace -> role (in that namespace)
List<String> 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<HasMetadata, Role>(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;
}

}
Original file line number Diff line number Diff line change
@@ -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<T extends HasMetadata> implements OnAddFilter<T> {
@Override
public boolean accept(T resource) {
return resource.getMetadata().getDeletionTimestamp() != null;
}

}
Original file line number Diff line number Diff line change
@@ -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<T extends HasMetadata> implements OnUpdateFilter<T> {

@Override
public boolean accept(T newResource, T oldResource) {
return newResource.getMetadata().getDeletionTimestamp() != null;
}
}
2 changes: 1 addition & 1 deletion operator-framework-junit5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>java-operator-sdk</artifactId>
<groupId>io.javaoperatorsdk</groupId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
2 changes: 1 addition & 1 deletion operator-framework/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>java-operator-sdk</artifactId>
<groupId>io.javaoperatorsdk</groupId>
<version>4.6.1-SNAPSHOT</version>
<version>4.7.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Void, Void>
implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -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<NamespaceDeletionTestCustomResource>, TestExecutionInfoProvider,
Cleaner<NamespaceDeletionTestCustomResource> {

public static final int CLEANER_WAIT_PERIOD = 300;
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);

@Override
public UpdateControl<NamespaceDeletionTestCustomResource> reconcile(
NamespaceDeletionTestCustomResource resource,
Context<NamespaceDeletionTestCustomResource> context) {
numberOfExecutions.addAndGet(1);
return UpdateControl.noUpdate();
}

public int getNumberOfExecutions() {
return numberOfExecutions.get();
}

@Override
public DeleteControl cleanup(NamespaceDeletionTestCustomResource resource,
Context<NamespaceDeletionTestCustomResource> context) {
try {
Thread.sleep(CLEANER_WAIT_PERIOD);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return DeleteControl.defaultDelete();
}
}
Loading
Loading