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 1127bf9 [SPARK-57543] Make ConfigOption dynamic override opt-in and
filter dynamic config refresh
1127bf9 is described below
commit 1127bf9fee08b55251401b86c847e6646b520c98
Author: Zhou JIANG <[email protected]>
AuthorDate: Tue Jul 7 17:03:44 2026 -0700
[SPARK-57543] Make ConfigOption dynamic override opt-in and filter dynamic
config refresh
### What changes were proposed in this pull request?
This PR makes runtime dynamic config override opt-in and enforces it
centrally when dynamic config is refreshed:
1. `ConfigOption`
- `enableDynamicOverride` now defaults to `false` — an option must
explicitly opt in via `.enableDynamicOverride(true)` to be overridable at
runtime.
- Each option registers itself (keyed by its config key) into a static
registry on construction, exposing `dynamicOverrideEnabledKeys()` as the
allow-list of overridable keys. `enableDynamicOverride` on each option remains
the single source of truth.
2. `SparkOperatorConfManager.refresh(Map)` now filters the incoming
ConfigMap data against `ConfigOption.dynamicOverrideEnabledKeys()`, applying
only overridable keys and logging any dropped (unknown or non-overridable) keys
at WARN. `getValue(String)` and `getAll()` therefore no longer surface override
values for keys that are never honored.
3. `SparkOperatorConf` options now set `enableDynamicOverride`explicitly;
this also fixes docs for `LOG_CONF` and `PERIODIC_GC_INTERVAL_SECONDS`, which
are only read at startup and are non-overridable.
The existing per-option check in `ConfigOption.resolveValue()` is kept as
defense-in-depth.
### Why are the changes needed?
Previously `refresh()` stored the entire ConfigMap into the override map,
and `enableDynamicOverride` defaulted to `true`. As a result, an override value
could be stored (and surfaced via `getValue`/`getAll`/the `LOG_CONF` dump) for
keys that are not meant to be hot-reloaded, and any new option was overridable
by default. Defaulting to opt-in and dropping non-allow-listed keys at refresh
time makes the override surface explicit, safer, and self-documenting, and
avoids silently ignored [...]
### Does this PR introduce _any_ user-facing change?
Yes. Dynamic config updates for keys that are not explicitly marked
`enableDynamicOverride(true)` are now ignored (and logged at WARN) instead of
being stored. In particular `spark.logConf` and
`spark.kubernetes.operator.periodicGC.intervalSeconds` are no longer
dynamically overridable (both are only read at startup). The generated config
documentation's "Allow Hot Reloading" column reflects each option's flag.
### How was this patch tested?
Updated `ConfigOptionTest` and `SparkOperatorConfManagerTest`, and added
`SparkOperatorConfManagerTest#testRefreshDropsUnknownAndNonOverridableKeys`
covering the allow-list filtering. Existing
`SparkOperatorConfigMapReconcilerTest` continues to exercise the dynamic-config
refresh path with an overridable key.
### Was this patch authored or co-authored using generative AI tooling?
Co-Authored-By: Claude Opus 4.8
Closes #713 from jiangzho/conf_wl.
Authored-by: Zhou JIANG <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
---
docs/config_properties.md | 6 +-
.../spark/k8s/operator/config/ConfigOption.java | 70 ++++++++++++++++++----
.../k8s/operator/config/SparkOperatorConf.java | 20 +++++++
.../operator/config/SparkOperatorConfManager.java | 31 +++++++++-
.../spark/k8s/operator/SparkOperatorTest.java | 8 +++
.../k8s/operator/config/ConfigOptionTest.java | 12 +++-
.../config/SparkOperatorConfManagerTest.java | 39 +++++++++++-
.../apache/spark/k8s/operator/utils/UtilsTest.java | 8 ++-
8 files changed, 174 insertions(+), 20 deletions(-)
diff --git a/docs/config_properties.md b/docs/config_properties.md
index 9c14468..1a9ff64 100644
--- a/docs/config_properties.md
+++ b/docs/config_properties.md
@@ -35,7 +35,7 @@
| spark.kubernetes.operator.metrics.sanitizePrometheusMetricsNameEnabled |
Boolean | true | false | Whether or not to enable automatic name sanitizing for
all metrics based on best-practice guide from Prometheus
https://prometheus.io/docs/practices/naming/ |
| spark.kubernetes.operator.name | String | spark-kubernetes-operator | false
| Name of the operator. |
| spark.kubernetes.operator.namespace | String | default | false | Namespace
that operator is deployed within. |
- | spark.kubernetes.operator.periodicGC.intervalSeconds | Long | 1800 | true |
Interval (in seconds) between periodic System.gc() invocations. Set to 0 or a
negative value to disable. Note that System.gc() is a no-op if JVM is started
with -XX:+DisableExplicitGC. |
+ | spark.kubernetes.operator.periodicGC.intervalSeconds | Long | 1800 | false
| Interval (in seconds) between periodic System.gc() invocations. Set to 0 or a
negative value to disable. Note that System.gc() is a no-op if JVM is started
with -XX:+DisableExplicitGC. |
| spark.kubernetes.operator.reconciler.appStatusListenerClassNames | String |
| false | Comma-separated names of SparkAppStatusListener class
implementations |
| spark.kubernetes.operator.reconciler.clusterStatusListenerClassNames |
String | | false | Comma-separated names of SparkClusterStatusListener class
implementations |
| spark.kubernetes.operator.reconciler.foregroundRequestTimeoutSeconds | Long
| 60 | true | Timeout (in seconds) for requests made to API server. This
applies only to foreground requests. |
@@ -50,6 +50,6 @@
| spark.kubernetes.operator.reconciler.terminationTimeoutSeconds | Integer |
30 | false | Grace period for operator shutdown before reconciliation threads
are killed. |
| spark.kubernetes.operator.reconciler.trimStateTransitionHistoryEnabled |
Boolean | true | true | When enabled, operator would trim state transition
history when a new attempt starts, keeping previous attempt summary only. |
| spark.kubernetes.operator.watchedNamespaces | String | default | true |
Comma-separated list of namespaces that the operator would be watching for
Spark resources. If set to '*', operator would watch all namespaces. |
- | spark.logConf | Boolean | false | true | When enabled, operator will print
configurations |
- | spark.redaction.regex | String | (?i)secret\|password\|token\|access[.]?key
| true | Regex to decide which parts of configuration properties contain
sensitive information, whose values would be redacted in operator logs. This
reuses Spark's redaction configuration key and default pattern. |
+ | spark.logConf | Boolean | false | false | When enabled, operator will print
configurations |
+ | spark.redaction.regex | String | (?i)secret\|password\|token\|access[.]?key
| false | Regex to decide which parts of configuration properties contain
sensitive information, whose values would be redacted in operator logs. This
reuses Spark's redaction configuration key and default pattern. |
diff --git
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/ConfigOption.java
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/ConfigOption.java
index 1130866..d9c4aae 100644
---
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/ConfigOption.java
+++
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/ConfigOption.java
@@ -19,12 +19,15 @@
package org.apache.spark.k8s.operator.config;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
import com.fasterxml.jackson.core.JsonProcessingException;
-import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
-import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@@ -34,20 +37,67 @@ import org.apache.spark.k8s.operator.utils.StringUtils;
/**
* Config options for Spark Operator. Supports primitive and serialized JSON.
*
+ * <p>Every option registers itself, keyed by {@link #key}, into a static
registry on construction.
+ * {@link SparkOperatorConfManager#refresh(Map)} consults {@link
#dynamicOverrideEnabledKeys()} to
+ * drop any dynamic-config update that targets an unknown key or one whose
{@link
+ * #enableDynamicOverride} is {@code false}.
+ *
* @param <T> The type of the config option's value.
*/
-@RequiredArgsConstructor
-@AllArgsConstructor
@EqualsAndHashCode
@ToString
-@Builder
@Slf4j
public class ConfigOption<T> {
- @Getter @Builder.Default private final boolean enableDynamicOverride = true;
- @Getter private String key;
- @Getter private String description;
- @Getter private T defaultValue;
- @Getter private Class<T> typeParameterClass;
+ /** Indexes every declared option so dynamic-config refresh can check
overridability. */
+ private static final Map<String, ConfigOption<?>> REGISTRY = new
ConcurrentHashMap<>();
+
+ /**
+ * Whether this option may be overridden at runtime via dynamic config.
Defaults to {@code false}
+ * (opt-in): dynamic override must be explicitly enabled through the builder.
+ */
+ @Getter private final boolean enableDynamicOverride;
+ @Getter private final String key;
+ @Getter private final String description;
+ @Getter private final T defaultValue;
+ @Getter private final Class<T> typeParameterClass;
+
+ @Builder
+ ConfigOption(
+ boolean enableDynamicOverride,
+ String key,
+ String description,
+ T defaultValue,
+ Class<T> typeParameterClass) {
+ this.enableDynamicOverride = enableDynamicOverride;
+ this.key = key;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ this.typeParameterClass = typeParameterClass;
+ if (StringUtils.isNotEmpty(key)) {
+ REGISTRY.put(key, this);
+ }
+ }
+
+ /**
+ * Returns the keys of all declared options that permit runtime dynamic
override. Used as the
+ * allow-list when refreshing dynamic config.
+ *
+ * <p><b>Note:</b> the returned set only includes options that have already
been constructed.
+ * Options are {@code static final} fields (e.g. on {@link
SparkOperatorConf}) that register
+ * themselves lazily on class initialization, so a caller that reads this
before those classes
+ * are initialized would see an empty or partial set. {@link
+ * SparkOperatorConfManager#refresh(Map)} forces initialization via {@link
+ * SparkOperatorConf#ensureOptionsRegistered()} first to avoid that; other
callers must ensure
+ * the relevant option classes are initialized.
+ *
+ * @return An immutable set of dynamically overridable config keys.
+ */
+ public static Set<String> dynamicOverrideEnabledKeys() {
+ return REGISTRY.values().stream()
+ .filter(ConfigOption::isEnableDynamicOverride)
+ .map(ConfigOption::getKey)
+ .collect(Collectors.toUnmodifiableSet());
+ }
/**
* Returns the resolved value of the config option.
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 03ab285..a1a4c10 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
@@ -35,6 +35,7 @@ public final class SparkOperatorConf {
public static final ConfigOption<Boolean> LOG_CONF =
ConfigOption.<Boolean>builder()
.key("spark.logConf")
+ .enableDynamicOverride(false)
.description("When enabled, operator will print configurations")
.typeParameterClass(Boolean.class)
.defaultValue(false)
@@ -48,6 +49,9 @@ public final class SparkOperatorConf {
public static final ConfigOption<String> REDACTION_REGEX =
ConfigOption.<String>builder()
.key("spark.redaction.regex")
+ // Intentionally not dynamically overridable: a runtime override of
the redaction
+ // pattern could weaken or disable log redaction, so this is only
read at startup.
+ .enableDynamicOverride(false)
.description(
"Regex to decide which parts of configuration properties contain
sensitive "
+ "information, whose values would be redacted in operator
logs. This reuses "
@@ -63,6 +67,7 @@ public final class SparkOperatorConf {
public static final ConfigOption<Long> PERIODIC_GC_INTERVAL_SECONDS =
ConfigOption.<Long>builder()
.key("spark.kubernetes.operator.periodicGC.intervalSeconds")
+ .enableDynamicOverride(false)
.description(
"Interval (in seconds) between periodic System.gc() invocations.
"
+ "Set to 0 or a negative value to disable. Note that
System.gc() is a no-op "
@@ -868,4 +873,19 @@ public final class SparkOperatorConf {
}
return value;
}
+
+ /**
+ * Forces static initialization of this class so that every {@link
ConfigOption} declared here
+ * has registered itself with {@link ConfigOption}'s registry.
+ *
+ * <p>{@link ConfigOption#dynamicOverrideEnabledKeys()} only reflects
options that have already
+ * been constructed, and the options are {@code static final} fields that
register lazily on
+ * class init. Invoking this method (an active use of the class) guarantees
the allow-list is
+ * complete before dynamic config is refreshed, independent of operator boot
ordering — see
+ * {@link SparkOperatorConfManager#refresh(java.util.Map)}. This method is
intentionally a no-op;
+ * the side effect is triggering class initialization.
+ */
+ public static void ensureOptionsRegistered() {
+ // No-op: the mere invocation triggers this class's static initializer.
+ }
}
diff --git
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManager.java
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManager.java
index a0f4bfe..15f8118 100644
---
a/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManager.java
+++
b/spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManager.java
@@ -23,8 +23,11 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import lombok.extern.slf4j.Slf4j;
@@ -99,14 +102,36 @@ public class SparkOperatorConfManager {
}
/**
- * Refreshes the configuration overrides with new values from a map.
+ * Refreshes the configuration overrides with new values from a map. Only
keys that are declared
+ * as dynamically overridable options (see {@link
ConfigOption#dynamicOverrideEnabledKeys()}) are
+ * applied; unknown keys and keys whose option disables dynamic override are
dropped and logged so
+ * an ineffective update is not silently ignored.
*
* @param updatedConfig A map containing the updated configuration
properties.
*/
public void refresh(Map<String, String> updatedConfig) {
+ // Ensure every ConfigOption has registered before we read the allow-list.
The options are
+ // static final fields on SparkOperatorConf and register lazily on class
init, so without this
+ // the allow-list could be empty or partial if refresh() runs before
SparkOperatorConf is
+ // touched, silently dropping every override. Forcing init here makes
correctness independent
+ // of boot ordering rather than relying on an earlier incidental read of
SparkOperatorConf.
+ SparkOperatorConf.ensureOptionsRegistered();
+ Set<String> allowedKeys = ConfigOption.dynamicOverrideEnabledKeys();
+ Properties filtered = new Properties();
+ List<String> droppedKeys = new ArrayList<>();
+ for (Map.Entry<String, String> entry : updatedConfig.entrySet()) {
+ if (allowedKeys.contains(entry.getKey())) {
+ filtered.put(entry.getKey(), entry.getValue());
+ } else {
+ droppedKeys.add(entry.getKey());
+ }
+ }
+ if (!droppedKeys.isEmpty()) {
+ log.warn(
+ "Ignoring dynamic config override for unknown or non-overridable
keys: {}", droppedKeys);
+ }
synchronized (this) {
- this.configOverrides = new Properties();
- configOverrides.putAll(updatedConfig);
+ this.configOverrides = filtered;
}
}
diff --git
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/SparkOperatorTest.java
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/SparkOperatorTest.java
index 7c55c33..7e97d92 100644
---
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/SparkOperatorTest.java
+++
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/SparkOperatorTest.java
@@ -53,6 +53,7 @@ import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.apache.spark.k8s.operator.client.KubernetesClientFactory;
+import org.apache.spark.k8s.operator.config.ConfigOption;
import org.apache.spark.k8s.operator.config.DynamicConfigMonitor;
import org.apache.spark.k8s.operator.config.SparkOperatorConf;
import org.apache.spark.k8s.operator.config.SparkOperatorConfManager;
@@ -285,6 +286,13 @@ class SparkOperatorTest {
MockedConstruction<KubernetesMetricsInterceptor>
interceptorMockedConstruction =
mockConstruction(KubernetesMetricsInterceptor.class)) {
setConfigKey(SparkOperatorConf.LOG_CONF, true);
+ // Register the sensitive key as dynamically overridable so refresh()
keeps it; refresh now
+ // drops any key that is not in the dynamic-override allow-list.
+ ConfigOption.<String>builder()
+ .key("spark.dummy.db.password")
+ .enableDynamicOverride(true)
+ .typeParameterClass(String.class)
+ .build();
SparkOperatorConfManager.INSTANCE.refresh(
Map.of("spark.dummy.db.password", "super-sensitive-value"));
mockMetricsSystemFactory
diff --git
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/ConfigOptionTest.java
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/ConfigOptionTest.java
index d1a9827..c380cc3 100644
---
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/ConfigOptionTest.java
+++
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/ConfigOptionTest.java
@@ -129,10 +129,10 @@ class ConfigOptionTest {
configOverride.put("foodouble", "10.0");
configOverride.put("foobool", "true");
configOverride.put("foo", "barbar");
- SparkOperatorConfManager.INSTANCE.refresh(configOverride);
ConfigOption<String> testStrConf =
ConfigOption.<String>builder()
.key("foo")
+ .enableDynamicOverride(true)
.typeParameterClass(String.class)
.description("foo foo.")
.defaultValue(defaultStringValue)
@@ -140,6 +140,7 @@ class ConfigOptionTest {
ConfigOption<Integer> testIntConf =
ConfigOption.<Integer>builder()
.key("fooint")
+ .enableDynamicOverride(true)
.typeParameterClass(Integer.class)
.description("foo foo.")
.defaultValue(defaultIntValue)
@@ -147,6 +148,7 @@ class ConfigOptionTest {
ConfigOption<Short> testShortConf =
ConfigOption.<Short>builder()
.key("fooshort")
+ .enableDynamicOverride(true)
.typeParameterClass(Short.class)
.description("foo foo.")
.defaultValue(defaultShortValue)
@@ -154,6 +156,7 @@ class ConfigOptionTest {
ConfigOption<Long> testLongConf =
ConfigOption.<Long>builder()
.key("foolong")
+ .enableDynamicOverride(true)
.typeParameterClass(Long.class)
.description("foo foo.")
.defaultValue(defaultLongValue)
@@ -161,6 +164,7 @@ class ConfigOptionTest {
ConfigOption<Boolean> testBooleanConf =
ConfigOption.<Boolean>builder()
.key("foobool")
+ .enableDynamicOverride(true)
.typeParameterClass(Boolean.class)
.description("foo foo.")
.defaultValue(defaultBooleanValue)
@@ -168,6 +172,7 @@ class ConfigOptionTest {
ConfigOption<Float> testFloatConf =
ConfigOption.<Float>builder()
.key("foofloat")
+ .enableDynamicOverride(true)
.typeParameterClass(Float.class)
.description("foo foo.")
.defaultValue(defaultFloatValue)
@@ -175,6 +180,7 @@ class ConfigOptionTest {
ConfigOption<Double> testDoubleConf =
ConfigOption.<Double>builder()
.key("foodouble")
+ .enableDynamicOverride(true)
.typeParameterClass(Double.class)
.description("foo foo.")
.defaultValue(defaultDoubleValue)
@@ -182,10 +188,14 @@ class ConfigOptionTest {
ConfigOption<Byte> testByteConf =
ConfigOption.<Byte>builder()
.key("foobyte")
+ .enableDynamicOverride(true)
.typeParameterClass(Byte.class)
.description("foo foo.")
.defaultValue(defaultByteValue)
.build();
+ // The options above opt in to dynamic override, so refreshing after they
are built lets the
+ // allow-list filter retain these keys.
+ SparkOperatorConfManager.INSTANCE.refresh(configOverride);
Assertions.assertEquals(overrideStringValue, testStrConf.getValue());
Assertions.assertEquals(overrideIntValue, testIntConf.getValue());
Assertions.assertEquals(overrideLongValue, testLongConf.getValue());
diff --git
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManagerTest.java
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManagerTest.java
index 7d0538f..924b6e0 100644
---
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManagerTest.java
+++
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/config/SparkOperatorConfManagerTest.java
@@ -54,6 +54,7 @@ class SparkOperatorConfManagerTest {
String propBackUp = System.getProperty("spark.kubernetes.operator.foo");
System.setProperty("spark.kubernetes.operator.foo", "bar");
try {
+ registerDynamicOption("spark.kubernetes.operator.foo");
SparkOperatorConfManager confManager = new SparkOperatorConfManager();
Assertions.assertEquals("bar",
confManager.getInitialValue("spark.kubernetes.operator.foo"));
Assertions.assertEquals("bar",
confManager.getValue("spark.kubernetes.operator.foo"));
@@ -80,8 +81,10 @@ class SparkOperatorConfManagerTest {
String propBackUp = System.getProperty("spark.kubernetes.operator.foo");
System.setProperty("spark.kubernetes.operator.foo", "bar");
try {
+ registerDynamicOption("spark.kubernetes.operator.foo");
+ registerDynamicOption("k1");
+ registerDynamicOption("k2");
SparkOperatorConfManager confManager = new SparkOperatorConfManager();
-
// Check initial configurations.
int initialSize = confManager.getAll().size();
Assertions.assertEquals(initialSize, confManager.initialConfig.size());
@@ -111,4 +114,38 @@ class SparkOperatorConfManagerTest {
}
}
}
+
+ @Test
+ void testRefreshDropsUnknownAndNonOverridableKeys() {
+ String overridableKey = "spark.kubernetes.operator.test.overridable";
+ String nonOverridableKey = "spark.kubernetes.operator.test.nonOverridable";
+ String unknownKey = "spark.kubernetes.operator.test.unknown";
+ registerDynamicOption(overridableKey);
+ ConfigOption.<String>builder()
+ .key(nonOverridableKey)
+ .enableDynamicOverride(false)
+ .typeParameterClass(String.class)
+ .build();
+
+ SparkOperatorConfManager confManager = new SparkOperatorConfManager();
+ confManager.refresh(
+ Map.of(
+ overridableKey, "kept",
+ nonOverridableKey, "dropped",
+ unknownKey, "dropped"));
+
+ Assertions.assertEquals(1, confManager.configOverrides.size());
+ Assertions.assertEquals("kept", confManager.getValue(overridableKey));
+ Assertions.assertNull(confManager.getValue(nonOverridableKey));
+ Assertions.assertNull(confManager.getValue(unknownKey));
+ }
+
+ /** Builds a throwaway {@link ConfigOption} so {@code key} is registered as
overridable. */
+ private static void registerDynamicOption(String key) {
+ ConfigOption.<String>builder()
+ .key(key)
+ .enableDynamicOverride(true)
+ .typeParameterClass(String.class)
+ .build();
+ }
}
diff --git
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/UtilsTest.java
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/UtilsTest.java
index 40e7e50..42c591d 100644
---
a/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/UtilsTest.java
+++
b/spark-operator/src/test/java/org/apache/spark/k8s/operator/utils/UtilsTest.java
@@ -94,14 +94,18 @@ class UtilsTest {
}
@Test
- void redactSensitiveInfoHonorsRedactionRegexFromOperatorConfig() {
+ void redactSensitiveInfoIgnoresDynamicRedactionRegexOverride() {
+ // spark.redaction.regex is intentionally not dynamically overridable: a
runtime override
+ // could weaken or disable redaction, so refresh() drops it and the
startup default pattern
+ // continues to apply.
SparkOperatorConfManager.INSTANCE.refresh(Map.of(REDACTION_REGEX_KEY,
"(?i)confidential"));
Map<String, String> props =
Map.of(
"custom.confidential.conf", "custom-value",
"spark.kubernetes.operator.namespace", "default");
Map<String, String> redacted = Utils.redactSensitiveInfo(props);
- assertEquals(REDACTED_TEXT, redacted.get("custom.confidential.conf"));
+ // The override is ignored, so "confidential" is not treated as sensitive.
+ assertEquals("custom-value", redacted.get("custom.confidential.conf"));
assertEquals("default",
redacted.get("spark.kubernetes.operator.namespace"));
assertEquals(props.keySet(), redacted.keySet());
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]