diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java
index e6aa6cbce6..ac5b7cd468 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java
@@ -18,7 +18,6 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.NamespaceChangeable;
-import io.javaoperatorsdk.operator.api.event.EventRecorder;
import io.javaoperatorsdk.operator.health.ControllerHealthInfo;
public interface RegisteredController
extends NamespaceChangeable {
@@ -26,17 +25,4 @@ public interface RegisteredController
extends NamespaceCh
ControllerConfiguration
getConfiguration();
ControllerHealthInfo getControllerHealthInfo();
-
- /**
- * Returns the {@link EventRecorder} of this controller, to record Kubernetes events outside of a
- * reconciliation, for example from a status listener or a background task. Within a
- * reconciliation, use {@link io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder()}
- * instead.
- *
- * @return the event recorder associated with this controller
- */
- default EventRecorder eventRecorder() {
- throw new UnsupportedOperationException(
- "This implementation of RegisteredController does not provide an EventRecorder");
- }
}
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 0b1d6b47cb..d42cf3c6fc 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
@@ -35,6 +35,7 @@
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.javaoperatorsdk.operator.api.event.DefaultEventRecorder;
+import io.javaoperatorsdk.operator.api.event.EventRecorder;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
@@ -295,6 +296,25 @@ default String clusterScopedEventNamespace() {
return DefaultEventRecorder.CLUSTER_SCOPED_EVENT_NAMESPACE;
}
+ /**
+ * The {@link EventRecorder} the controllers of the operator record their Kubernetes events
+ * through, to plug in a custom implementation, for example one that assembles events differently
+ * by extending {@link DefaultEventRecorder}, or one that records them somewhere else entirely.
+ *
+ *
When empty, which is the default, every controller gets a {@link DefaultEventRecorder} of
+ * its own, which attributes the events it records to that controller. A recorder configured here
+ * is shared by all controllers of the operator, so it decides on its own what the events it
+ * records are attributed to, and it is up to the caller to hold on to the instance if it also
+ * records events outside of a reconciliation.
+ *
+ * @return the event recorder to use for the whole operator, or an empty optional to let each
+ * controller use its own default one
+ */
+ @Experimental(Experimental.API_MIGHT_CHANGE)
+ default Optional eventRecorder() {
+ 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
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 9f0fd78356..944200c742 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
@@ -27,6 +27,7 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.api.event.EventRecorder;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory;
@@ -48,6 +49,7 @@ public class ConfigurationServiceOverrider {
private ExecutorService workflowExecutorService;
private LeaderElectionConfiguration leaderElectionConfiguration;
private String clusterScopedEventNamespace;
+ private EventRecorder eventRecorder;
private InformerStoppedHandler informerStoppedHandler;
private Boolean stopOnInformerErrorDuringStartup;
private Duration cacheSyncTimeout;
@@ -148,6 +150,25 @@ public ConfigurationServiceOverrider withClusterScopedEventNamespace(String name
return this;
}
+ /**
+ * Replaces the {@link EventRecorder} the controllers of the operator record their Kubernetes
+ * events through by the specified one, which is then shared by all of them. Use this to record
+ * events differently, for example through a subclass of {@link
+ * io.javaoperatorsdk.operator.api.event.DefaultEventRecorder} that assembles them another way, or
+ * to hold on to the recorder in order to also record events outside of a reconciliation.
+ *
+ * When not set, every controller records its events through a recorder of its own, which
+ * attributes them to that controller.
+ *
+ * @param eventRecorder the event recorder to use for the whole operator
+ * @return this {@link ConfigurationServiceOverrider} for chained customization
+ */
+ @Experimental(Experimental.API_MIGHT_CHANGE)
+ public ConfigurationServiceOverrider withEventRecorder(EventRecorder eventRecorder) {
+ this.eventRecorder = eventRecorder;
+ return this;
+ }
+
public ConfigurationServiceOverrider withInformerStoppedHandler(InformerStoppedHandler handler) {
this.informerStoppedHandler = handler;
return this;
@@ -297,6 +318,11 @@ public String clusterScopedEventNamespace() {
: original.clusterScopedEventNamespace();
}
+ @Override
+ public Optional eventRecorder() {
+ return eventRecorder != null ? Optional.of(eventRecorder) : original.eventRecorder();
+ }
+
@Override
public Optional getInformerStoppedHandler() {
return informerStoppedHandler != null
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/event/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/event/EventRecorder.java
index a7d66d8e54..c4d4919508 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/event/EventRecorder.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/event/EventRecorder.java
@@ -16,20 +16,25 @@
package io.javaoperatorsdk.operator.api.event;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.api.reconciler.Experimental;
+
+import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;
/**
* Records Kubernetes events on behalf of a controller.
*
* This is the unbound form of the API: it is scoped to a controller, not to a reconciliation,
* and can therefore be used outside of the reconciliation loop, for example from a status listener
- * or a background task. Obtain it from {@link
- * io.javaoperatorsdk.operator.RegisteredController#eventRecorder()}. Within a reconciliation,
- * prefer {@link io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder()}, which is
- * already bound to the primary resource.
+ * or a background task. To use it that way, configure the instance the operator records its events
+ * through, see {@link io.javaoperatorsdk.operator.api.config.ConfigurationService#eventRecorder()},
+ * and keep a reference to it. Within a reconciliation, prefer {@link
+ * io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder()}, which is already bound to
+ * the primary resource.
*
*
Recording an event is best effort: failures to write the event to the cluster are logged and
* swallowed, and never fail the caller.
*/
+@Experimental(API_MIGHT_CHANGE)
public interface EventRecorder {
/**
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
index df9c19b263..7c10c0963e 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
@@ -216,8 +216,9 @@ default Optional getSecondaryResource(
/**
* Returns a {@link ResourceEventRecorder} bound to the primary resource, to record Kubernetes
- * events about it. To record events outside of a reconciliation, or about another object, use
- * {@link io.javaoperatorsdk.operator.RegisteredController#eventRecorder()}.
+ * events about it. To record events outside of a reconciliation, or about another object, use an
+ * {@link io.javaoperatorsdk.operator.api.event.EventRecorder} configured for the operator, see
+ * {@link io.javaoperatorsdk.operator.api.config.ConfigurationService#eventRecorder()}.
*
* @return an event recorder bound to the primary resource
* @throws UnsupportedOperationException if the implementation does not provide an event recorder
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java
index 285eb3988c..bdd858e813 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java
@@ -115,15 +115,19 @@ public Controller(
this.kubernetesClient = kubernetesClient;
this.metrics = Optional.ofNullable(configurationService.getMetrics()).orElse(Metrics.NOOP);
this.eventRecorder =
- new DefaultEventRecorder(
- configuration.getName(),
- configurationService
- .getLeaderElectionConfiguration()
- .flatMap(LeaderElectionConfiguration::getIdentity)
- .orElseGet(DefaultEventRecorder::defaultReportingInstance),
- Optional.ofNullable(configurationService.clusterScopedEventNamespace())
- .orElse(DefaultEventRecorder.CLUSTER_SCOPED_EVENT_NAMESPACE),
- new DefaultEventSink(kubernetesClient));
+ configurationService
+ .eventRecorder()
+ .orElseGet(
+ () ->
+ new DefaultEventRecorder(
+ configuration.getName(),
+ configurationService
+ .getLeaderElectionConfiguration()
+ .flatMap(LeaderElectionConfiguration::getIdentity)
+ .orElseGet(DefaultEventRecorder::defaultReportingInstance),
+ Optional.ofNullable(configurationService.clusterScopedEventNamespace())
+ .orElse(DefaultEventRecorder.CLUSTER_SCOPED_EVENT_NAMESPACE),
+ new DefaultEventSink(kubernetesClient)));
contextInitializer = reconciler instanceof ContextInitializer;
isCleaner = reconciler instanceof Cleaner;
@@ -358,7 +362,11 @@ public ControllerHealthInfo getControllerHealthInfo() {
return controllerHealthInfo;
}
- @Override
+ /**
+ * The {@link EventRecorder} this controller records its Kubernetes events through, either the one
+ * configured for the operator, see {@link ConfigurationService#eventRecorder()}, or a {@link
+ * DefaultEventRecorder} of its own.
+ */
public EventRecorder eventRecorder() {
return eventRecorder;
}
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
index 9df62bc03c..9d5db71c6f 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java
@@ -23,6 +23,9 @@
import org.junit.jupiter.api.Test;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.api.event.EventRecord;
+import io.javaoperatorsdk.operator.api.event.EventRecorder;
+import io.javaoperatorsdk.operator.api.event.ResourceEventRecorder;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import static org.assertj.core.api.Assertions.assertThat;
@@ -106,6 +109,28 @@ public R clone(R object) {
config.reconciliationTerminationTimeout(), overridden.reconciliationTerminationTimeout());
}
+ @Test
+ void eventRecorderIsNotConfiguredByDefaultAndCanBeOverridden() {
+ final var eventRecorder =
+ new EventRecorder() {
+ @Override
+ public void record(HasMetadata regarding, EventRecord event) {}
+
+ @Override
+ public ResourceEventRecorder forResource(HasMetadata regarding) {
+ return null;
+ }
+ };
+
+ assertThat(config.eventRecorder()).isEmpty();
+ assertThat(
+ new ConfigurationServiceOverrider(config)
+ .withEventRecorder(eventRecorder)
+ .build()
+ .eventRecorder())
+ .contains(eventRecorder);
+ }
+
@Test
void threadCountConfiguredProperly() {
final var overridden =
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java
index b725f49132..de582f593d 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java
@@ -28,6 +28,8 @@
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec;
+import io.javaoperatorsdk.operator.api.event.DefaultEventRecorder;
+import io.javaoperatorsdk.operator.api.event.EventRecorder;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;
@@ -110,6 +112,36 @@ void doesNotNotifyMetricsWhenEventProcessorNotStarted() {
verify(metrics, never()).eventProcessingStarted(controller);
}
+ @Test
+ void recordsEventsThroughTheEventRecorderConfiguredForTheOperator() {
+ final var client = MockKubernetesClient.client(Secret.class);
+ final var eventRecorder = mock(EventRecorder.class);
+ final var configurationService =
+ ConfigurationService.newOverriddenConfigurationService(
+ new BaseConfigurationService(),
+ o -> o.withEventRecorder(eventRecorder).withKubernetesClient(client));
+ final var configuration =
+ MockControllerConfiguration.forResource(Secret.class, configurationService);
+
+ final var controller = new Controller(reconciler, configuration, client);
+
+ assertThat(controller.eventRecorder()).isSameAs(eventRecorder);
+ }
+
+ @Test
+ void recordsEventsThroughAnEventRecorderOfItsOwnWhenNoneIsConfigured() {
+ final var client = MockKubernetesClient.client(Secret.class);
+ final var configuration =
+ MockControllerConfiguration.forResource(
+ Secret.class,
+ ConfigurationService.newOverriddenConfigurationService(
+ new BaseConfigurationService(), o -> o.withKubernetesClient(client)));
+
+ final var controller = new Controller(reconciler, configuration, client);
+
+ assertThat(controller.eventRecorder()).isInstanceOf(DefaultEventRecorder.class);
+ }
+
@Test
void crdShouldNotBeCheckedForCustomResourcesIfDisabled() {
final var client = MockKubernetesClient.client(TestCustomResource.class);