diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 794bc11d9a..0ebcef2d5c 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -38,6 +38,7 @@ public abstract class AbstractOperatorExtension public static final int DEFAULT_NAMESPACE_DELETE_TIMEOUT = 90; private final KubernetesClient kubernetesClient; + private final KubernetesClient infrastructureKubernetesClient; protected final List infrastructure; protected Duration infrastructureTimeout; protected final boolean oneNamespacePerClass; @@ -56,10 +57,15 @@ protected AbstractOperatorExtension( boolean preserveNamespaceOnError, boolean waitForNamespaceDeletion, KubernetesClient kubernetesClient, + KubernetesClient infrastructureKubernetesClient, Function namespaceNameSupplier, Function perClassNamespaceNameSupplier) { + this.infrastructureKubernetesClient = + infrastructureKubernetesClient != null + ? infrastructureKubernetesClient + : new KubernetesClientBuilder().build(); this.kubernetesClient = - kubernetesClient != null ? kubernetesClient : new KubernetesClientBuilder().build(); + kubernetesClient != null ? kubernetesClient : this.infrastructureKubernetesClient; this.infrastructure = infrastructure; this.infrastructureTimeout = infrastructureTimeout; this.oneNamespacePerClass = oneNamespacePerClass; @@ -94,6 +100,11 @@ public KubernetesClient getKubernetesClient() { return kubernetesClient; } + @Override + public KubernetesClient getInfrastructureKubernetesClient() { + return infrastructureKubernetesClient; + } + public String getNamespace() { return namespace; } @@ -141,7 +152,7 @@ protected void beforeEachImpl(ExtensionContext context) { protected void before(ExtensionContext context) { LOGGER.info("Initializing integration test in namespace {}", namespace); - kubernetesClient + infrastructureKubernetesClient .namespaces() .resource( new NamespaceBuilder() @@ -149,8 +160,8 @@ protected void before(ExtensionContext context) { .build()) .serverSideApply(); - kubernetesClient.resourceList(infrastructure).serverSideApply(); - kubernetesClient + infrastructureKubernetesClient.resourceList(infrastructure).serverSideApply(); + infrastructureKubernetesClient .resourceList(infrastructure) .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); } @@ -172,16 +183,19 @@ protected void after(ExtensionContext context) { if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { LOGGER.info("Preserving namespace {}", namespace); } else { - kubernetesClient.resourceList(infrastructure).delete(); + infrastructureKubernetesClient.resourceList(infrastructure).delete(); deleteOperator(); LOGGER.info("Deleting namespace {} and stopping operator", namespace); - kubernetesClient.namespaces().withName(namespace).delete(); + infrastructureKubernetesClient.namespaces().withName(namespace).delete(); if (waitForNamespaceDeletion) { LOGGER.info("Waiting for namespace {} to be deleted", namespace); Awaitility.await("namespace deleted") .pollInterval(50, TimeUnit.MILLISECONDS) .atMost(namespaceDeleteTimeout, TimeUnit.SECONDS) - .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); + .until( + () -> + infrastructureKubernetesClient.namespaces().withName(namespace).get() + == null); } } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/ClusterDeployedOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/ClusterDeployedOperatorExtension.java index 3fc49d4575..7273b464de 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/ClusterDeployedOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/ClusterDeployedOperatorExtension.java @@ -39,6 +39,7 @@ private ClusterDeployedOperatorExtension( boolean waitForNamespaceDeletion, boolean oneNamespacePerClass, KubernetesClient kubernetesClient, + KubernetesClient infrastructureKubernetesClient, Function namespaceNameSupplier, Function perClassNamespaceNameSupplier) { super( @@ -48,6 +49,7 @@ private ClusterDeployedOperatorExtension( preserveNamespaceOnError, waitForNamespaceDeletion, kubernetesClient, + infrastructureKubernetesClient, namespaceNameSupplier, perClassNamespaceNameSupplier); this.operatorDeployment = operatorDeployment; @@ -69,7 +71,7 @@ protected void before(ExtensionContext context) { final var crdPath = "./target/classes/META-INF/fabric8/"; final var crdSuffix = "-v1.yml"; - final var kubernetesClient = getKubernetesClient(); + final var kubernetesClient = getInfrastructureKubernetesClient(); for (var crdFile : Objects.requireNonNull( new File(crdPath).listFiles((ignored, name) -> name.endsWith(crdSuffix)))) { @@ -107,13 +109,17 @@ protected void before(ExtensionContext context) { @Override protected void deleteOperator() { - getKubernetesClient().resourceList(operatorDeployment).inNamespace(namespace).delete(); + getInfrastructureKubernetesClient() + .resourceList(operatorDeployment) + .inNamespace(namespace) + .delete(); } public static class Builder extends AbstractBuilder { private final List operatorDeployment; private Duration deploymentTimeout; private KubernetesClient kubernetesClient; + private KubernetesClient infrastructureKubernetesClient; protected Builder() { super(); @@ -150,7 +156,18 @@ public Builder withKubernetesClient(KubernetesClient kubernetesClient) { return this; } + public Builder withInfrastructureKubernetesClient(KubernetesClient kubernetesClient) { + this.infrastructureKubernetesClient = kubernetesClient; + return this; + } + public ClusterDeployedOperatorExtension build() { + infrastructureKubernetesClient = + infrastructureKubernetesClient != null + ? infrastructureKubernetesClient + : new KubernetesClientBuilder().build(); + kubernetesClient = + kubernetesClient != null ? kubernetesClient : infrastructureKubernetesClient; return new ClusterDeployedOperatorExtension( operatorDeployment, deploymentTimeout, @@ -159,7 +176,8 @@ public ClusterDeployedOperatorExtension build() { preserveNamespaceOnError, waitForNamespaceDeletion, oneNamespacePerClass, - kubernetesClient != null ? kubernetesClient : new KubernetesClientBuilder().build(), + kubernetesClient, + infrastructureKubernetesClient, namespaceNameSupplier, perClassNamespaceNameSupplier); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java index d93032333f..683e9e03fc 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java @@ -4,4 +4,6 @@ public interface HasKubernetesClient { KubernetesClient getKubernetesClient(); + + KubernetesClient getInfrastructureKubernetesClient(); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index 54cb57544d..3d4f81de3c 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -66,6 +66,7 @@ private LocallyRunOperatorExtension( boolean waitForNamespaceDeletion, boolean oneNamespacePerClass, KubernetesClient kubernetesClient, + KubernetesClient infrastructureKubernetesClient, Consumer configurationServiceOverrider, Function namespaceNameSupplier, Function perClassNamespaceNameSupplier, @@ -78,6 +79,7 @@ private LocallyRunOperatorExtension( preserveNamespaceOnError, waitForNamespaceDeletion, kubernetesClient, + infrastructureKubernetesClient, namespaceNameSupplier, perClassNamespaceNameSupplier); this.reconcilers = reconcilers; @@ -240,7 +242,7 @@ public Operator getOperator() { protected void before(ExtensionContext context) { super.before(context); - final var kubernetesClient = getKubernetesClient(); + final var kubernetesClient = getInfrastructureKubernetesClient(); for (var ref : portForwards) { String podName = @@ -313,7 +315,7 @@ protected void before(ExtensionContext context) { protected void after(ExtensionContext context) { super.after(context); - var kubernetesClient = getKubernetesClient(); + var kubernetesClient = getInfrastructureKubernetesClient(); var iterator = appliedCRDs.iterator(); while (iterator.hasNext()) { @@ -365,6 +367,7 @@ public static class Builder extends AbstractBuilder { private final List additionalCRDs = new ArrayList<>(); private Consumer beforeStartHook; private KubernetesClient kubernetesClient; + private KubernetesClient infrastructureKubernetesClient; protected Builder() { super(); @@ -419,6 +422,12 @@ public Builder withKubernetesClient(KubernetesClient kubernetesClient) { return this; } + public Builder withInfrastructureKubernetesClient( + KubernetesClient infrastructureKubernetesClient) { + this.infrastructureKubernetesClient = infrastructureKubernetesClient; + return this; + } + public Builder withAdditionalCustomResourceDefinition( Class customResource) { additionalCustomResourceDefinitions.add(customResource); @@ -452,6 +461,7 @@ public LocallyRunOperatorExtension build() { waitForNamespaceDeletion, oneNamespacePerClass, kubernetesClient, + infrastructureKubernetesClient, configurationServiceOverrider, namespaceNameSupplier, perClassNamespaceNameSupplier, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java new file mode 100644 index 0000000000..7754b19eee --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java @@ -0,0 +1,134 @@ +package io.javaoperatorsdk.operator.baseapi.infrastructureclient; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRole; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +class InfrastructureClientIT { + + private static final String RBAC_TEST_ROLE = "rbac-test-role.yaml"; + private static final String RBAC_TEST_ROLE_BINDING = "rbac-test-role-binding.yaml"; + private static final String RBAC_TEST_USER = "rbac-test-user"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new InfrastructureClientTestReconciler()) + .withKubernetesClient( + new KubernetesClientBuilder() + .withConfig(new ConfigBuilder().withImpersonateUsername(RBAC_TEST_USER).build()) + .build()) + .withInfrastructureKubernetesClient( + new KubernetesClientBuilder().build()) // no limitations + .build(); + + /** + * We need to apply the cluster role also before the CRD deployment so the rbac-test-user is + * permitted to deploy it + */ + public InfrastructureClientIT() { + applyClusterRole(RBAC_TEST_ROLE); + applyClusterRoleBinding(RBAC_TEST_ROLE_BINDING); + } + + @BeforeEach + void setup() { + applyClusterRole(RBAC_TEST_ROLE); + applyClusterRoleBinding(RBAC_TEST_ROLE_BINDING); + } + + @AfterEach + void cleanup() { + removeClusterRoleBinding(RBAC_TEST_ROLE_BINDING); + removeClusterRole(RBAC_TEST_ROLE); + } + + @Test + void canCreateInfrastructure() { + var resource = new InfrastructureClientTestCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder().withName("infrastructure-client-resource").build()); + operator.create(resource); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + InfrastructureClientTestCustomResource r = + operator.get( + InfrastructureClientTestCustomResource.class, + "infrastructure-client-resource"); + assertThat(r).isNotNull(); + }); + + assertThat( + operator + .getReconcilerOfType(InfrastructureClientTestReconciler.class) + .getNumberOfExecutions()) + .isEqualTo(1); + } + + @Test + void shouldNotAccessNotPermittedResources() { + assertThatThrownBy( + () -> + operator + .getKubernetesClient() + .apiextensions() + .v1() + .customResourceDefinitions() + .list()) + .isInstanceOf(KubernetesClientException.class) + .hasMessageContaining( + "User \"%s\" cannot list resource \"customresourcedefinitions\"" + .formatted(RBAC_TEST_USER)); + + // but we should be able to access all resources with the infrastructure client + var deploymentList = + operator + .getInfrastructureKubernetesClient() + .apiextensions() + .v1() + .customResourceDefinitions() + .list(); + assertThat(deploymentList).isNotNull(); + } + + private void applyClusterRoleBinding(String filename) { + var clusterRoleBinding = + ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).serverSideApply(); + } + + private void applyClusterRole(String filename) { + var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + operator.getInfrastructureKubernetesClient().resource(clusterRole).serverSideApply(); + } + + private void removeClusterRoleBinding(String filename) { + var clusterRoleBinding = + ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).delete(); + } + + private void removeClusterRole(String filename) { + var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + operator.getInfrastructureKubernetesClient().resource(clusterRole).delete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestCustomResource.java new file mode 100644 index 0000000000..65e0738daa --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.infrastructureclient; + +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("ict") +public class InfrastructureClientTestCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestReconciler.java new file mode 100644 index 0000000000..a906fd7d0e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientTestReconciler.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.baseapi.infrastructureclient; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(name = InfrastructureClientTestReconciler.TEST_RECONCILER) +public class InfrastructureClientTestReconciler + implements Reconciler, TestExecutionInfoProvider { + + private static final Logger log = + LoggerFactory.getLogger(InfrastructureClientTestReconciler.class); + + public static final String TEST_RECONCILER = "InfrastructureClientTestReconciler"; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + InfrastructureClientTestCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + log.info("Reconciled for: {}", ResourceID.fromResource(resource)); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role-binding.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role-binding.yaml new file mode 100644 index 0000000000..64e577c4c1 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role-binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: rbac-test-role-binding +subjects: + - kind: User + name: rbac-test-user + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: rbac-test-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role.yaml new file mode 100644 index 0000000000..9c1d790d94 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/baseapi/infrastructureclient/rbac-test-role.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rbac-test-role +rules: + - apiGroups: [ "apiextensions.k8s.io"] + resources: [ "customresourcedefinitions" ] + verbs: [ "create", "update", "patch", "delete", "deletecollection" ] # explicitly don't include "list" for the test + - apiGroups: [ "sample.javaoperatorsdk" ] + resources: [ "infrastructureclienttestcustomresources" ] + verbs: [ "get", "list", "watch", "create", "update", "patch", "delete", "deletecollection" ]