This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ca60092 [SPARK-60008] Enable K8s events by default
1ca60092 is described below

commit 1ca60092e30dba0ed2fc92ab2d6da27a4297667c
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Tue Oct 6 15:20:43 2026 -0700

    [SPARK-60008] Enable K8s events by default
    
    ### What changes were proposed in this pull request?
    
    This PR aims to enable K8s events by default.
    
    ### Why are the changes needed?
    
    Currently, users see no K8s events unless the operator administrator 
enables them, while other operators like Kubeflow Spark Operator, Flink 
Kubernetes Operator and Argo Workflows publish events by default. The known 
downsides of enabling them were addressed by SPARK-59926 and SPARK-59931.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Unlike 
[1.0.0](https://github.com/apache/spark-kubernetes-operator/releases/tag/1.0.0) 
(2026-07-23), the operator publishes K8s `Event` objects into the namespaces of 
`SparkApplication` and `SparkCluster` resources by default. To restore the 
previous behavior, set `spark.kubernetes.operator.events.enabled` to `false`.
    
    ### How was this patch tested?
    
    Pass the CIs with the updated test cases.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5.5
    
    Closes #941 from dongjoon-hyun/SPARK-60008.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 docs/config_properties.md                          |  2 +-
 docs/configuration.md                              |  8 ++++--
 docs/migration_guide.md                            | 10 +++++++
 docs/spark_custom_resources.md                     | 10 +++----
 .../k8s/operator/config/SparkOperatorConf.java     |  6 ++--
 .../operator/utils/ConfigurableEventRecorder.java  |  4 +--
 .../ConfigurableEventRecorderDefaultSinkTest.java  | 32 ++++++++++++++++++++++
 .../utils/ConfigurableEventRecorderTest.java       | 17 +++++++-----
 tests/e2e/events/chainsaw-test.yaml                |  5 ++--
 tests/e2e/helm/events-config-values.yaml           |  1 -
 tests/e2e/helm/kueue-config-values.yaml            |  4 ---
 tests/e2e/suspend-events/chainsaw-test.yaml        |  4 +--
 12 files changed, 73 insertions(+), 30 deletions(-)

diff --git a/docs/config_properties.md b/docs/config_properties.md
index 83f920f9..60153a86 100644
--- a/docs/config_properties.md
+++ b/docs/config_properties.md
@@ -16,7 +16,7 @@
  | spark.kubernetes.operator.dynamicConfig.reloadIntervalSeconds | Long | 60 | 
false | Interval (in seconds) at which the dynamic config file is re-read. Used 
by the 'file' source. | 
  | spark.kubernetes.operator.dynamicConfig.selector | String | 
app.kubernetes.io/name=spark-kubernetes-operator,app.kubernetes.io/component=operator-dynamic-config-overrides
 | false | The selector str applied to dynamic config map. Used by the 
'configMap' source. | 
  | spark.kubernetes.operator.dynamicConfig.source | String | configMap | false 
| Source of dynamic config overrides when 
spark.kubernetes.operator.dynamicConfig.enabled is true. Supported values: 
'configMap' (default) watches a ConfigMap via a Kubernetes informer and 
requires RBAC to read ConfigMaps; 'file' periodically reloads a properties file 
mounted from a ConfigMap and requires no extra RBAC. | 
- | spark.kubernetes.operator.events.enabled | Boolean | false | true | When 
enabled, operator would publish Kubernetes Event objects into the namespace of 
the Spark resource they describe, making them visible to users of that 
namespace via 'kubectl describe' and 'kubectl get events'. Events are published 
when a resource transitions into a new state, as Warning for failure states and 
Normal for most other states, and on other occasions, e.g. when an operation on 
a resource fails, or while [...]
+ | spark.kubernetes.operator.events.enabled | Boolean | true | true | When 
enabled, operator would publish Kubernetes Event objects into the namespace of 
the Spark resource they describe, making them visible to users of that 
namespace via 'kubectl describe' and 'kubectl get events'. Events are published 
when a resource transitions into a new state, as Warning for failure states and 
Normal for most other states, and on other occasions, e.g. when an operation on 
a resource fails, or while  [...]
  | spark.kubernetes.operator.events.excludedReasons | String |  | true | 
Comma-separated list of Java regular expressions of Kubernetes Event reasons 
that operator would not publish even when 
'spark.kubernetes.operator.events.enabled' is true, e.g. 
'RunningHealthy,RunningWithPartialCapacity' or 'Running.*'. Each expression 
must match the whole reason and is case-sensitive. An invalid expression 
matches only the reason identical to it. If empty, operator would publish 
events of all reasons. | 
  | spark.kubernetes.operator.events.minIntervalSeconds | Long | 300 | true | 
Minimum interval (in seconds) between two Kubernetes Events with the same 
reason and the same message on the same Spark resource. The first event of a 
reason on a resource is always published right away, only a repeat that says 
exactly what the last one said is dropped. Such a repeat only bumps the 'count' 
of the one Event object, so it carries no information while it still costs a 
read and a write on the API se [...]
  | spark.kubernetes.operator.health.probePort | Integer | 19091 | false | The 
port used for health/readiness check probe status. | 
diff --git a/docs/configuration.md b/docs/configuration.md
index a67ee0fc..591262c9 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -57,9 +57,11 @@ operatorConfiguration:
 
 ## Kubernetes Events
 
-When `spark.kubernetes.operator.events.enabled` is `true`, the operator 
publishes Kubernetes
-`Event` objects about `SparkApplication` and `SparkCluster` resources into 
their namespaces. They
-are visible via `kubectl describe` and `kubectl get events`.
+By default, the operator publishes Kubernetes `Event` objects about 
`SparkApplication` and
+`SparkCluster` resources into their namespaces. They are visible via `kubectl 
describe` and
+`kubectl get events`. To publish none, set 
`spark.kubernetes.operator.events.enabled` to `false`.
+This option supports dynamic override. To skip only some reasons, use
+`spark.kubernetes.operator.events.excludedReasons` instead, as described below.
 
 Publishing them requires the `get`, `create` and `patch` permissions on 
`events` in the core API
 group in each namespace of the Spark resources. Without them, the operator 
logs a
diff --git a/docs/migration_guide.md b/docs/migration_guide.md
index 7175caa0..5a72d48f 100644
--- a/docs/migration_guide.md
+++ b/docs/migration_guide.md
@@ -122,6 +122,16 @@ affect building the project, CI, tests or examples.
   `NullPointerException` for most properties. The operator logs each invalid 
value once as a
   warning, instead of as an error with a stack trace whenever it reads the 
value. Use `true` or
   `false`, which behave the same in both versions 
([SPARK-XXXXX](https://issues.apache.org/jira/browse/SPARK-XXXXX)).
+- Since 1.1.0, the operator publishes Kubernetes `Event` objects about 
`SparkApplication` and
+  `SparkCluster` resources into their namespaces, e.g. whenever a resource 
transitions into a new
+  state, since `spark.kubernetes.operator.events.enabled` is `true` by 
default. 1.0 published
+  none. Publishing them requires the `get`, `create` and `patch` permissions 
on `events` in the
+  core API group, which the Helm chart grants. Without them, e.g. if you 
manage the RBAC of the
+  operator yourself, the operator logs a `Could not record ... event` warning 
for each event. An
+  event exporter may also forward the `Warning` events, such as `Failed`, to 
chat or alerting
+  services outside the cluster. To restore the behavior before 1.1.0, set
+  `spark.kubernetes.operator.events.enabled` to `false`. See
+  [Kubernetes Events](configuration.md#kubernetes-events) for details.
 
 ### SparkApplication
 
diff --git a/docs/spark_custom_resources.md b/docs/spark_custom_resources.md
index 0ddd26c8..673973b8 100644
--- a/docs/spark_custom_resources.md
+++ b/docs/spark_custom_resources.md
@@ -540,7 +540,7 @@ running `SparkCluster` is stopped instead, as described 
below.
 `suspend: true`, the initial `Submitted` status is not persisted to the API 
server, so
 `kubectl get` shows an empty `Current State` and no state transition events 
are published until
 initialization resumes. Instead, the `SuspendHeld` 
[event](configuration.md#kubernetes-events) is
-published when enabled. Since the status is not there to fall back on, it is 
republished every 30
+published by default. Since the status is not there to fall back on, it is 
republished every 30
 minutes by default while the hold lasts, so that it outlives the event 
retention of the API
 server. An application held later, in `ScheduledToRestart`, keeps the status 
its previous attempt
 already wrote and gets the same event, since that status says that a restart 
is due, not that the
@@ -569,7 +569,7 @@ spec:
   HorizontalPodAutoscaler and PodDisruptionBudget of its workers if any. Its 
Services and
   NetworkPolicy are kept. Any application running on the cluster is terminated 
with it, since there
   is no graceful decommission. A failure to delete them, other than one which 
may clear on its own,
-  publishes the `SuspendReleaseFailed` 
[event](configuration.md#kubernetes-events) when enabled.
+  publishes the `SuspendReleaseFailed` 
[event](configuration.md#kubernetes-events) by default.
 * Setting it back to `false` moves the cluster to `Submitted` once its master 
and worker pods are
   gone and its Kueue `Workload`, if any, is released, and the master and 
workers are requested
   again from scratch, like a newly created cluster. Only pods labeled with the 
`master` or `worker`
@@ -624,7 +624,7 @@ spec:
   label is ignored, so the resource is not queued and starts right away, and 
the operator does
   not access any Kueue resource, like Kueue ignores a job whose integration is 
not enabled. Since
   the author of the resource may not see the operator configuration, the 
`KueueDisabled` warning
-  [event](configuration.md#kubernetes-events) is published when enabled.
+  [event](configuration.md#kubernetes-events) is published by default.
 * The `Workload` is named `<lower-cased kind>-<resource name>` and is owned by 
the Spark resource,
   so it is garbage collected along with it.
 * Like Kueue built-in integrations, the `Workload` gets the priority of the 
`WorkloadPriorityClass`
@@ -641,7 +641,7 @@ spec:
   Like `spec.suspend`, the initial `Submitted` status of the first attempt is 
not persisted to the
   API server, so `kubectl get` shows an empty `Current State` and no state 
transition events are
   published until the `Workload` is admitted. Instead, the 
`KueueAdmissionPending` and
-  `KueueAdmitted` [events](configuration.md#kubernetes-events) are published 
when enabled. Since
+  `KueueAdmitted` [events](configuration.md#kubernetes-events) are published 
by default. Since
   the status is not there to fall back on, the pending event is republished 
while the `Workload`
   waits, so that it outlives the event retention of the API server. Use 
`kubectl get workload` to
   see the admission status. If the spec changes while waiting, the
@@ -787,7 +787,7 @@ spec:
   `spark.executor.instances`, or a deactivation. The operator checks the 
admission only before it
   creates the resources, so after a later eviction the driver and executors 
keep running, and
   until the attempt stops, the `KueueEvictionIgnored` 
[event](configuration.md#kubernetes-events)
-  is published instead when enabled. An evicted `Workload` keeps its Kueue 
quota until the driver
+  is published instead by default. An evicted `Workload` keeps its Kueue quota 
until the driver
   and executors are released, so a workload which preempts the application 
waits until then,
   while a deactivated one no longer counts against it. Since `spec.suspend` 
does not stop a
   running attempt, delete the application to release its quota earlier. With 
`blockAdmission`, an
diff --git 
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConf.java
 
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConf.java
index d53e8766..39e2ccf5 100644
--- 
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConf.java
+++ 
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConf.java
@@ -337,9 +337,11 @@ public final class SparkOperatorConf {
                   + "spec.suspend or Kueue. See the 'Kubernetes Events' 
section of "
                   + "docs/configuration.md for all reasons and the required 
RBAC permissions. "
                   + "These are Event resources in the core "
-                  + "API group, unrelated to the internal events that trigger 
reconciliation.")
+                  + "API group, unrelated to the internal events that trigger 
reconciliation. "
+                  + "If false, operator would publish no event. To skip only 
some reasons, use "
+                  + "'spark.kubernetes.operator.events.excludedReasons' 
instead.")
           .typeParameterClass(Boolean.class)
-          .defaultValue(false)
+          .defaultValue(true)
           .build();
 
   /**
diff --git 
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorder.java
 
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorder.java
index 5170a53b..ec014d38 100644
--- 
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorder.java
+++ 
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorder.java
@@ -47,8 +47,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.spark.k8s.operator.client.KubernetesClientFactory;
 
 /**
- * An {@link EventRecorder} that drops every event unless {@link
- * 
org.apache.spark.k8s.operator.config.SparkOperatorConf#KUBERNETES_EVENTS_ENABLED}
 is set, drops
+ * An {@link EventRecorder} that drops every event while {@link
+ * 
org.apache.spark.k8s.operator.config.SparkOperatorConf#KUBERNETES_EVENTS_ENABLED}
 is false, drops
  * events whose reason matches one of the patterns in {@link
  * 
org.apache.spark.k8s.operator.config.SparkOperatorConf#KUBERNETES_EVENTS_EXCLUDED_REASONS},
 and
  * drops an event repeated on the same resource with the same reason and the 
same message within
diff --git 
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderDefaultSinkTest.java
 
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderDefaultSinkTest.java
index 2fc6d1c0..c63b45b8 100644
--- 
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderDefaultSinkTest.java
+++ 
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderDefaultSinkTest.java
@@ -19,10 +19,12 @@
 
 package org.apache.spark.k8s.operator.utils;
 
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
 import static org.apache.spark.k8s.operator.Constants.HTTP_TOO_MANY_REQUESTS;
 import static 
org.apache.spark.k8s.operator.config.SparkOperatorConf.KUBERNETES_EVENTS_ENABLED;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
@@ -38,6 +40,7 @@ import 
io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.event.EventRecord;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -79,6 +82,35 @@ class ConfigurableEventRecorderDefaultSinkTest {
         .containsExactly("published");
   }
 
+  @Test
+  void makesNoRequestWhileDisabled() {
+    
SparkOperatorConfManager.INSTANCE.refresh(Map.of(KUBERNETES_EVENTS_ENABLED.getKey(),
 "false"));
+    ConfigurableEventRecorder recorder = 
ConfigurableEventRecorder.withDefaultSink(client);
+
+    recorder.record(EventRecord.warning("ReconcileError", "dropped"), 
appContext());
+
+    assertThat(server.getRequestCount()).isZero();
+  }
+
+  @Test
+  void dropsAForbiddenWriteWithoutThrowing() {
+    // Without the RBAC permission to create events, every write is rejected, 
which must not throw
+    // out of the reconciliation that records the event.
+    
SparkOperatorConfManager.INSTANCE.refresh(Map.of(KUBERNETES_EVENTS_ENABLED.getKey(),
 "true"));
+    ConfigurableEventRecorder recorder = 
ConfigurableEventRecorder.withDefaultSink(client);
+    server
+        .expect()
+        .post()
+        .withPath("/api/v1/namespaces/ns-1/events")
+        .andReturn(HTTP_FORBIDDEN, null)
+        .always();
+
+    assertThatCode(
+            () -> recorder.record(EventRecord.warning("ReconcileError", 
"dropped"), appContext()))
+        .doesNotThrowAnyException();
+    
assertThat(client.v1().events().inNamespace("ns-1").list().getItems()).isEmpty();
+  }
+
   /** Returns a context from which the JOSDK recorder can build an Event about 
app-1 in ns-1. */
   private static Context<?> appContext() {
     SparkApplication app = new SparkApplication();
diff --git 
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderTest.java
 
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderTest.java
index 445d556a..b9b3fcb0 100644
--- 
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderTest.java
+++ 
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/ConfigurableEventRecorderTest.java
@@ -98,15 +98,16 @@ class ConfigurableEventRecorderTest {
 
   @Test
   void dropsEventsWhileDisabled() {
-    // The option defaults to false, so an operator that never opts in writes 
nothing.
+    setEventsEnabled(false);
+
     recorder.record(EventRecord.warning("ReconcileError", "boom"), context);
 
     verifyNoInteractions(delegate);
   }
 
   @Test
-  void forwardsEventsWhileEnabled() {
-    setEventsEnabled(true);
+  void forwardsEventsByDefault() {
+    // The option defaults to true, so an operator that never configures it 
publishes events.
     EventRecord event = EventRecord.warning("ReconcileError", "boom");
 
     recorder.record(event, context);
@@ -118,6 +119,7 @@ class ConfigurableEventRecorderTest {
   void boundRecorderIsGatedOnTheFlagRatherThanOnBindingTime() {
     // Bound while disabled, then enabled mid reconciliation: the dynamic 
override has to take
     // effect, which it only does if the flag is read per event and not when 
forContext is called.
+    setEventsEnabled(false);
     var bound = recorder.forContext(context);
     bound.warn("ReconcileError", "dropped");
     verifyNoInteractions(delegate);
@@ -144,15 +146,16 @@ class ConfigurableEventRecorderTest {
   }
 
   @Test
-  void treatsAMalformedOverrideAsDisabled() {
-    // A malformed override falls back to the default value, which is 
disabled. Publishing an
+  void treatsAMalformedOverrideAsEnabled() {
+    // A malformed override falls back to the default value, which is enabled. 
Publishing an
     // event must never be the thing that throws out of a reconciliation.
     SparkOperatorConfManager.INSTANCE.refresh(
         Map.of(KUBERNETES_EVENTS_ENABLED.getKey(), "null"));
+    EventRecord event = EventRecord.warning("ReconcileError", "boom");
 
-    recorder.record(EventRecord.warning("ReconcileError", "boom"), context);
+    recorder.record(event, context);
 
-    verifyNoInteractions(delegate);
+    verify(delegate).record(event, context);
   }
 
   @Test
diff --git a/tests/e2e/events/chainsaw-test.yaml 
b/tests/e2e/events/chainsaw-test.yaml
index 790fadd5..aba2985e 100644
--- a/tests/e2e/events/chainsaw-test.yaml
+++ b/tests/e2e/events/chainsaw-test.yaml
@@ -18,9 +18,8 @@
 # Asserts the events published by the state transitions of a SparkApplication 
which succeeds, a
 # SparkApplication which fails, and a SparkCluster.
 #
-# Like the `suspend-events` suite, this suite requires an operator installed 
with
-# `spark.kubernetes.operator.events.enabled=true`, as the `events` CI mode 
does with
-# tests/e2e/helm/events-config-values.yaml. Run it locally with:
+# Like the `suspend-events` suite, this suite runs in the `events` CI mode, 
which installs the
+# operator with tests/e2e/helm/events-config-values.yaml. Run it locally with:
 #
 #   helm install spark --create-namespace \
 #     -f build-tools/helm/spark-kubernetes-operator/values.yaml \
diff --git a/tests/e2e/helm/events-config-values.yaml 
b/tests/e2e/helm/events-config-values.yaml
index 141b2bd3..f92f1705 100644
--- a/tests/e2e/helm/events-config-values.yaml
+++ b/tests/e2e/helm/events-config-values.yaml
@@ -15,7 +15,6 @@
 
 operatorConfiguration:
   spark-operator.properties: |+
-    spark.kubernetes.operator.events.enabled=true
     # The suspend-events suite asserts that a held resource republishes its 
SuspendHeld event.
     # The default is 1800 seconds, which no assert can wait for.
     spark.kubernetes.operator.reconciler.suspendHoldRequeueIntervalSeconds=10
diff --git a/tests/e2e/helm/kueue-config-values.yaml 
b/tests/e2e/helm/kueue-config-values.yaml
index 6fd65be8..604e4957 100644
--- a/tests/e2e/helm/kueue-config-values.yaml
+++ b/tests/e2e/helm/kueue-config-values.yaml
@@ -26,7 +26,3 @@ operatorDeployment:
 operatorRbac:
   kueue:
     enabled: true
-
-operatorConfiguration:
-  spark-operator.properties: |+
-    spark.kubernetes.operator.events.enabled=true
diff --git a/tests/e2e/suspend-events/chainsaw-test.yaml 
b/tests/e2e/suspend-events/chainsaw-test.yaml
index f9176b38..e73db445 100644
--- a/tests/e2e/suspend-events/chainsaw-test.yaml
+++ b/tests/e2e/suspend-events/chainsaw-test.yaml
@@ -18,8 +18,8 @@
 # Asserts the `SuspendHeld` event of a held SparkApplication and SparkCluster.
 #
 # Unlike the `suspend` suite, which covers the hold itself with the shipped 
defaults, this suite
-# requires an operator installed with 
`spark.kubernetes.operator.events.enabled=true`, as the
-# `events` CI mode does with tests/e2e/helm/events-config-values.yaml. Run it 
locally with:
+# requires an operator installed with 
tests/e2e/helm/events-config-values.yaml, as the `events`
+# CI mode does. Run it locally with:
 #
 #   helm install spark --create-namespace \
 #     -f build-tools/helm/spark-kubernetes-operator/values.yaml \


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to