diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e7ba5d1efc..e5627d1e0a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -53,3 +53,12 @@ jobs: driver: 'docker' - name: Run integration tests run: ./mvnw ${MAVEN_ARGS} -B package -P no-unit-tests --file pom.xml + - name: Adjust Minikube Min Request Timeout Setting + uses: manusa/actions-setup-minikube@v2.7.0 + with: + minikube version: 'v1.26.0' + kubernetes version: ${{ matrix.kubernetes }} + driver: 'docker' + start args: '--extra-config=apiserver.min-request-timeout=3' + - name: Run Special Integration Tests + run: ./mvnw ${MAVEN_ARGS} -B package -P minimal-watch-timeout-dependent-it --file pom.xml \ No newline at end of file diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 72099f7fcb..16faa0e9f0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -152,6 +153,35 @@ default Optional getLeaderElectionConfiguration() { return Optional.empty(); } + /** + *

+ * if true, operator stops if there are some issues with informers + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource} or + * {@link io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource} + * on startup. Other event sources may also respect this flag. + *

+ *

+ * if false, the startup will ignore recoverable errors, caused for example by RBAC issues, and + * will try to reconnect periodically in the background. + *

+ */ + default boolean stopOnInformerErrorDuringStartup() { + return true; + } + + /** + * Timeout for cache sync in milliseconds. In other words source start timeout. Note that is + * "stopOnInformerErrorDuringStartup" is true the operator will stop on timeout. Default is 2 + * minutes. + */ + default Duration cacheSyncTimeout() { + return Duration.ofMinutes(2); + } + + /** + * Handler for an informer stop. Informer stops if there is a non-recoverable error. Like received + * a resource that cannot be deserialized. + */ default Optional getInformerStoppedHandler() { return Optional.of((informer, ex) -> { if (ex != null) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index ee442c07fa..2b8aee9708 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -27,6 +28,8 @@ public class ConfigurationServiceOverrider { private ExecutorService workflowExecutorService; private LeaderElectionConfiguration leaderElectionConfiguration; private InformerStoppedHandler informerStoppedHandler; + private Boolean stopOnInformerErrorDuringStartup; + private Duration cacheSyncTimeout; ConfigurationServiceOverrider(ConfigurationService original) { this.original = original; @@ -99,6 +102,17 @@ public ConfigurationServiceOverrider withInformerStoppedHandler(InformerStoppedH return this; } + public ConfigurationServiceOverrider withStopOnInformerErrorDuringStartup( + boolean stopOnInformerErrorDuringStartup) { + this.stopOnInformerErrorDuringStartup = stopOnInformerErrorDuringStartup; + return this; + } + + public ConfigurationServiceOverrider withCacheSyncTimeout(Duration cacheSyncTimeout) { + this.cacheSyncTimeout = cacheSyncTimeout; + return this; + } + public ConfigurationService build() { return new BaseConfigurationService(original.getVersion(), cloner, objectMapper) { @Override @@ -171,6 +185,17 @@ public Optional getInformerStoppedHandler() { return informerStoppedHandler != null ? Optional.of(informerStoppedHandler) : original.getInformerStoppedHandler(); } + + @Override + public boolean stopOnInformerErrorDuringStartup() { + return stopOnInformerErrorDuringStartup != null ? stopOnInformerErrorDuringStartup + : super.stopOnInformerErrorDuringStartup(); + } + + @Override + public Duration cacheSyncTimeout() { + return cacheSyncTimeout != null ? cacheSyncTimeout : super.cacheSyncTimeout(); + } }; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 90bf01267c..601cb0c10c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -3,6 +3,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -11,6 +14,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ExceptionHandler; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; @@ -37,10 +41,9 @@ public InformerWrapper(SharedIndexInformer informer) { @Override public void start() throws OperatorException { try { - informer.run(); - + var configService = ConfigurationServiceProvider.instance(); // register stopped handler if we have one defined - ConfigurationServiceProvider.instance().getInformerStoppedHandler() + configService.getInformerStoppedHandler() .ifPresent(ish -> { final var stopped = informer.stopped(); if (stopped != null) { @@ -58,6 +61,27 @@ public void start() throws OperatorException { + fullResourceName + "/" + version); } }); + if (!configService.stopOnInformerErrorDuringStartup()) { + informer.exceptionHandler((b, t) -> !ExceptionHandler.isDeserializationException(t)); + } + try { + var start = informer.start(); + // note that in case we don't put here timeout and stopOnInformerErrorDuringStartup is + // false, and there is a rbac issue the get never returns; therefore operator never really + // starts + start.toCompletableFuture().get(configService.cacheSyncTimeout().toMillis(), + TimeUnit.MILLISECONDS); + } catch (TimeoutException | ExecutionException e) { + if (configService.stopOnInformerErrorDuringStartup()) { + log.error("Informer startup error. Operator will be stopped. Informer: {}", informer, e); + throw new OperatorException(e); + } else { + log.warn("Informer startup error. Will periodically retry. Informer: {}", informer, e); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } } catch (Exception e) { log.error("Couldn't start informer for " + versionedFullResourceName() + " resources", e); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java index 2589f566da..81f8741de9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java @@ -17,9 +17,8 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.*; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class MockKubernetesClient { @@ -44,8 +43,12 @@ public static KubernetesClient client(Class clazz, when(resources.inAnyNamespace()).thenReturn(inAnyNamespace); when(inAnyNamespace.withLabelSelector(nullable(String.class))).thenReturn(filterable); SharedIndexInformer informer = mock(SharedIndexInformer.class); + CompletableFuture informerStartRes = new CompletableFuture<>(); + informerStartRes.complete(null); + when(informer.start()).thenReturn(informerStartRes); CompletableFuture stopped = new CompletableFuture<>(); when(informer.stopped()).thenReturn(stopped); + when(informer.getApiTypeClass()).thenReturn(clazz); if (informerRunBehavior != null) { doAnswer(invocation -> { try { @@ -53,8 +56,8 @@ public static KubernetesClient client(Class clazz, } catch (Exception e) { stopped.completeExceptionally(e); } - return null; - }).when(informer).run(); + return stopped; + }).when(informer).start(); } doAnswer(invocation -> null).when(informer).stop(); Indexer mockIndexer = mock(Indexer.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index e941e8390d..8b5c0202c0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -19,6 +19,7 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -255,7 +256,10 @@ void informerStoppedHandlerShouldBeCalledWhenInformerStops() { MockKubernetesClient.client(Deployment.class, unused -> { throw exception; })); - informerEventSource.start(); + + // by default informer fails to start if there is an exception in the client on start. + // Throws the exception further. + assertThrows(RuntimeException.class, () -> informerEventSource.start()); verify(informerStoppedHandler, atLeastOnce()).onStop(any(), eq(exception)); } finally { ConfigurationServiceProvider.reset(); 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 8f4d62341c..e852455ebf 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 @@ -101,6 +101,10 @@ public RegisteredController getRegisteredControllerForReconcile( return registeredControllers.get(getReconcilerOfType(type)); } + public Operator getOperator() { + return operator; + } + @SuppressWarnings("unchecked") @Override protected void before(ExtensionContext context) { @@ -159,12 +163,20 @@ protected void before(ExtensionContext context) { } private void applyCrd(String resourceTypeName) { + applyCrd(resourceTypeName, getKubernetesClient()); + } + + public static void applyCrd(Class resourceClass, KubernetesClient client) { + applyCrd(ReconcilerUtils.getResourceTypeName(resourceClass), client); + } + + public static void applyCrd(String resourceTypeName, KubernetesClient client) { String path = "/META-INF/fabric8/" + resourceTypeName + "-v1.yml"; - try (InputStream is = getClass().getResourceAsStream(path)) { + try (InputStream is = LocallyRunOperatorExtension.class.getResourceAsStream(path)) { if (is == null) { throw new IllegalStateException("Cannot find CRD at " + path); } - final var crd = getKubernetesClient().load(is); + final var crd = client.load(is); crd.createOrReplace(); Thread.sleep(CRD_READY_WAIT); // readiness is not applicable for CRD, just wait a little LOGGER.debug("Applied CRD with path: {}", path); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java new file mode 100644 index 0000000000..bfc0e4c02f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java @@ -0,0 +1,237 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +import org.junit.jupiter.api.*; + +import io.fabric8.kubernetes.api.model.Namespace; +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.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestCustomResource; +import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * The test relies on a special minikube configuration: "min-request-timeout" to have a very low + * value, see: "minikube start --extra-config=apiserver.min-request-timeout=3" + * + *

+ * This is important when tests are affected by permission changes, since the watch permissions are + * just checked when established a watch request. So minimal request timeout is set to make sure + * that with periodical watch reconnect the permission is tested again. + *

+ *

+ * The test ends with "ITS" (Special) since it needs to run separately from other ITs + *

+ */ +class InformerRelatedBehaviorITS { + + public static final String TEST_RESOURCE_NAME = "test1"; + + KubernetesClient adminClient = new KubernetesClientBuilder().build(); + InformerRelatedBehaviorTestReconciler reconciler; + String actualNamespace; + volatile boolean stopHandlerCalled = false; + + @BeforeEach + void beforeEach(TestInfo testInfo) { + LocallyRunOperatorExtension.applyCrd(InformerRelatedBehaviorTestCustomResource.class, + adminClient); + testInfo.getTestMethod().ifPresent(method -> { + actualNamespace = KubernetesResourceUtil.sanitizeName(method.getName()); + adminClient.resource(namespace()).createOrReplace(); + }); + } + + @AfterEach + void cleanup() { + adminClient.resource(testCustomResource()).delete(); + adminClient.resource(namespace()).delete(); + } + + @Test + void notStartsUpWithoutPermissionIfInstructed() { + adminClient.resource(testCustomResource()).createOrReplace(); + setNoCustomResourceAccess(); + + assertThrows(OperatorException.class, () -> startOperator(true)); + assertNotReconciled(); + } + + @Test + void startsUpWhenNoPermissionToCustomResource() { + adminClient.resource(testCustomResource()).createOrReplace(); + setNoCustomResourceAccess(); + + startOperator(false); + assertNotReconciled(); + + setFullResourcesAccess(); + waitForWatchReconnect(); + assertReconciled(); + } + + @Test + void startsUpWhenNoPermissionToSecondaryResource() { + adminClient.resource(testCustomResource()).createOrReplace(); + setNoConfigMapAccess(); + + startOperator(false); + assertNotReconciled(); + + setFullResourcesAccess(); + waitForWatchReconnect(); + assertReconciled(); + } + + @Test + void resilientForLoosingPermissionForCustomResource() throws InterruptedException { + setFullResourcesAccess(); + startOperator(true); + setNoCustomResourceAccess(); + + waitForWatchReconnect(); + adminClient.resource(testCustomResource()).createOrReplace(); + + assertNotReconciled(); + + setFullResourcesAccess(); + assertReconciled(); + } + + + @Test + void resilientForLoosingPermissionForSecondaryResource() { + setFullResourcesAccess(); + startOperator(true); + setNoConfigMapAccess(); + + waitForWatchReconnect(); + adminClient.resource(testCustomResource()).createOrReplace(); + + await().pollDelay(Duration.ofMillis(300)).untilAsserted(() -> { + var cm = + adminClient.configMaps().inNamespace(actualNamespace).withName(TEST_RESOURCE_NAME).get(); + assertThat(cm).isNull(); + }); + + setFullResourcesAccess(); + assertReconciled(); + } + + @Test + void callsStopHandlerOnStartupFail() { + setNoCustomResourceAccess(); + adminClient.resource(testCustomResource()).createOrReplace(); + + assertThrows(OperatorException.class, () -> startOperator(true)); + + await().untilAsserted(() -> assertThat(stopHandlerCalled).isTrue()); + } + + private static void waitForWatchReconnect() { + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private void assertNotReconciled() { + await().pollDelay(Duration.ofMillis(2000)).untilAsserted(() -> { + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(0); + }); + } + + InformerRelatedBehaviorTestCustomResource testCustomResource() { + InformerRelatedBehaviorTestCustomResource testCustomResource = + new InformerRelatedBehaviorTestCustomResource(); + testCustomResource.setMetadata(new ObjectMetaBuilder() + .withNamespace(actualNamespace) + .withName(TEST_RESOURCE_NAME) + .build()); + return testCustomResource; + } + + private void assertReconciled() { + await().untilAsserted(() -> { + assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(0); + var cm = + adminClient.configMaps().inNamespace(actualNamespace).withName(TEST_RESOURCE_NAME).get(); + assertThat(cm).isNotNull(); + }); + } + + KubernetesClient clientUsingServiceAccount() { + KubernetesClient client = new KubernetesClientBuilder() + .withConfig(new ConfigBuilder() + .withImpersonateUsername("rbac-test-user") + .withNamespace(actualNamespace) + .build()) + .build(); + return client; + } + + Operator startOperator(boolean stopOnInformerErrorDuringStartup) { + ConfigurationServiceProvider.reset(); + reconciler = new InformerRelatedBehaviorTestReconciler(); + + Operator operator = new Operator(clientUsingServiceAccount(), + co -> { + co.withStopOnInformerErrorDuringStartup(stopOnInformerErrorDuringStartup); + co.withCacheSyncTimeout(Duration.ofMillis(3000)); + co.withInformerStoppedHandler((informer, ex) -> { + stopHandlerCalled = true; + }); + }); + operator.register(reconciler); + operator.installShutdownHook(); + operator.start(); + return operator; + } + + private void setNoConfigMapAccess() { + applyClusterRole("rback-test-no-configmap-access.yaml"); + applyClusterRoleBinding(); + } + + private void setNoCustomResourceAccess() { + applyClusterRole("rback-test-no-cr-access.yaml"); + applyClusterRoleBinding(); + } + + private void setFullResourcesAccess() { + applyClusterRole("rback-test-full-access-role.yaml"); + applyClusterRoleBinding(); + } + + private void applyClusterRoleBinding() { + var clusterRoleBinding = ReconcilerUtils + .loadYaml(ClusterRoleBinding.class, this.getClass(), "rback-test-role-binding.yaml"); + adminClient.resource(clusterRoleBinding).createOrReplace(); + } + + private void applyClusterRole(String filename) { + var clusterRole = ReconcilerUtils + .loadYaml(ClusterRole.class, this.getClass(), filename); + adminClient.resource(clusterRole).createOrReplace(); + } + + private Namespace namespace() { + Namespace n = new Namespace(); + n.setMetadata(new ObjectMetaBuilder() + .withName(actualNamespace) + .build()); + return n; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/ConfigMapDependentResource.java new file mode 100644 index 0000000000..c9747cbd00 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/ConfigMapDependentResource.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.sample.informerrelatedbehavior; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +@KubernetesDependent(labelSelector = "app=rbac-test") +public class ConfigMapDependentResource + extends CRUDKubernetesDependentResource { + + public static final String DATA_KEY = "key"; + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(InformerRelatedBehaviorTestCustomResource primary, + Context context) { + return new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder() + .withLabels(Map.of("app", "rbac-test")) + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withData(Map.of(DATA_KEY, primary.getMetadata().getName())) + .build(); + + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestCustomResource.java new file mode 100644 index 0000000000..fa59c15831 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.informerrelatedbehavior; + +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("rbt") +public class InformerRelatedBehaviorTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java new file mode 100644 index 0000000000..baeff08478 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.sample.informerrelatedbehavior; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.client.KubernetesClient; +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.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(dependents = @Dependent(type = ConfigMapDependentResource.class)) +public class InformerRelatedBehaviorTestReconciler + implements Reconciler, TestExecutionInfoProvider { + + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private KubernetesClient client; + + @Override + public UpdateControl reconcile( + InformerRelatedBehaviorTestCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-full-access-role.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-full-access-role.yaml new file mode 100644 index 0000000000..024f298462 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-full-access-role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + # "namespace" omitted since ClusterRoles are not namespaced + name: rbac-behavior +rules: + - apiGroups: [ "sample.javaoperatorsdk" ] + resources: [ "informerrelatedbehaviortestcustomresources" ] + verbs: [ "get", "watch", "list","post", "delete" ] + - apiGroups: [ "" ] + resources: [ "configmaps" ] + verbs: [ "get", "watch", "list","post", "delete", "create" ] + + diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-configmap-access.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-configmap-access.yaml new file mode 100644 index 0000000000..bac8c1149f --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-configmap-access.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + # "namespace" omitted since ClusterRoles are not namespaced + name: rbac-behavior +rules: + - apiGroups: [ "sample.javaoperatorsdk" ] + resources: [ "informerrelatedbehaviortestcustomresources" ] + verbs: [ "get", "watch", "list","post", "delete" ] + + diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-cr-access.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-cr-access.yaml new file mode 100644 index 0000000000..8c6ae85aac --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-no-cr-access.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + # "namespace" omitted since ClusterRoles are not namespaced + name: rbac-behavior +rules: + - apiGroups: [""] + resources: [ "configmaps" ] + verbs: [ "get", "watch", "list","post", "delete","create" ] + diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-role-binding.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-role-binding.yaml new file mode 100644 index 0000000000..5b9c9ee2d9 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/rback-test-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: ClusterRoleBinding +metadata: + name: read-secrets-global +subjects: + - kind: User + name: rbac-test-user + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: rbac-behavior + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/pom.xml b/pom.xml index 521ab7bffb..953b8befc6 100644 --- a/pom.xml +++ b/pom.xml @@ -307,6 +307,7 @@ **/*IT.java **/*E2E.java + WatchPermissionAwareTest @@ -377,6 +378,28 @@ + + + minimal-watch-timeout-dependent-it + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*ITS.java + + + **/*Test.java + **/*E2E.java + **/*IT.java + + + + + + end-to-end-tests @@ -401,7 +424,6 @@ release - org.apache.maven.plugins maven-surefire-plugin @@ -409,6 +431,7 @@ **/*IT.java **/*E2E.java + **/InformerRelatedBehaviorTest.java