This is an automated email from the ASF dual-hosted git repository.
tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 182e8fbcab [ZEPPELIN-5901] Sanitize interpreter pod user label to
satisfy Kubernetes label rules
182e8fbcab is described below
commit 182e8fbcab5cbca2d8d3629cd561a9302427a3a5
Author: HwangRock <[email protected]>
AuthorDate: Mon Oct 5 14:05:02 2026 +0900
[ZEPPELIN-5901] Sanitize interpreter pod user label to satisfy Kubernetes
label rules
### What this fixes
When a user's name has special characters (like a space), the interpreter
pod fails to launch because the value breaks the Kubernetes label rules.
This shows up with external auth like Shiro + OIDC/JWT, where the principal
is a display name / username / email — so it can easily contain spaces or other
special chars.
JIRA: https://issues.apache.org/jira/browse/ZEPPELIN-5901
### Why it happened
The pod name already gets normalized through `K8sUtils.generateK8sName`,
but the `user` label didn't go through anything — the principal was put into
the label almost as-is (only `.trim()`). So Kubernetes rejected the pod.
The root cause was basically an asymmetry: the pod name was sanitized, the
user label wasn't.
### How I fixed it
I added a label-specific sanitizer `K8sUtils.generateK8sLabelValue`,
following the same removal rule as the existing pod-name normalization, but
capping the length at 63 (the K8s label limit). The user label now goes through
it before being set, and if nothing usable is left (e.g. a blank principal) the
label is just omitted instead of writing an empty/`"null"` value.
### Reproduction
I reproduced it on a local kind cluster by mimicking the interpreter pod's
label block.
**As-is** — user label `"Firstname Lastname"` (contains a space):
```
The Pod "jdbc-repro-5901" is invalid: metadata.labels: Invalid value:
"Firstname Lastname": a valid label must be an empty string or consist of
alphanumeric characters, '-', '_' or '.', and must start and end with an
alphanumeric character (regex used for validation is
'(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')
```
**To-be** — same pod with the normalized value `"firstnamelastname"`:
```
pod/jdbc-repro-5901-fixed created
```
### Tests
- Unit tests for the sanitizer (space / special chars / over-63-char cut /
blank input).
- Rendered-spec tests that check the `user` label is normalized, and that
the label line is omitted when the principal is blank.
- All `k8s-standard` module tests pass (32 tests, 0 failures).
Closes #5529 from HwangRock/ZEPPELIN-5901-sanitize-user-label.
Signed-off-by: ChanHo Lee <[email protected]>
---
k8s/interpreter/100-interpreter-spec.yaml | 2 +
.../launcher/K8sRemoteInterpreterProcess.java | 5 +-
.../zeppelin/interpreter/launcher/K8sUtils.java | 24 +++++++++
.../launcher/K8sRemoteInterpreterProcessTest.java | 58 +++++++++++++++++++++-
.../interpreter/launcher/K8sUtilsTest.java | 27 ++++++++++
.../test/resources/k8s-specs/interpreter-spec.yaml | 2 +
6 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/k8s/interpreter/100-interpreter-spec.yaml
b/k8s/interpreter/100-interpreter-spec.yaml
index 0f18379662..7d3a43e4e7 100644
--- a/k8s/interpreter/100-interpreter-spec.yaml
+++ b/k8s/interpreter/100-interpreter-spec.yaml
@@ -23,7 +23,9 @@ metadata:
app: {{zeppelin.k8s.interpreter.pod.name}}
interpreterGroupId: {{zeppelin.k8s.interpreter.group.id}}
interpreterSettingName: {{zeppelin.k8s.interpreter.setting.name}}
+ {% if zeppelin.k8s.interpreter.user %}
user: {{ zeppelin.k8s.interpreter.user }}
+ {% endif %}
{% if zeppelin.k8s.server.uid is defined %}
ownerReferences:
- apiVersion: v1
diff --git
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
index 62fcc38df0..db41d8ac81 100644
---
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
+++
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
@@ -298,7 +298,10 @@ public class K8sRemoteInterpreterProcess extends
RemoteInterpreterManagedProcess
Properties k8sProperties = new Properties();
// k8s template properties
- k8sProperties.put("zeppelin.k8s.interpreter.user",
String.valueOf(userName).trim());
+ String userLabel = K8sUtils.generateK8sLabelValue(userName);
+ if (StringUtils.isNotEmpty(userLabel)) {
+ k8sProperties.put("zeppelin.k8s.interpreter.user", userLabel);
+ }
k8sProperties.put("zeppelin.k8s.interpreter.namespace",
getInterpreterNamespace());
k8sProperties.put("zeppelin.k8s.interpreter.pod.name", getPodName());
k8sProperties.put("zeppelin.k8s.interpreter.serviceAccount",
getServiceAccount());
diff --git
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sUtils.java
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sUtils.java
index f064842753..500e9da90c 100644
---
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sUtils.java
+++
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sUtils.java
@@ -174,4 +174,28 @@ public class K8sUtils {
}
return randomSuffix ? result + "-" +
RandomStringUtils.randomAlphabetic(6).toLowerCase() : result;
}
+
+ private static final int MAX_LABEL_VALUE_LENGTH = 63;
+
+ /**
+ * Generates a value for a Kubernetes label from an arbitrary string such as
a principal name.
+ *
+ * See
https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
+ *
+ * The value is lower-cased, characters other than [a-z0-9.-] are removed,
it is cut to 63
+ * characters and leading and trailing non alphanumeric characters are
dropped.
+ *
+ * @param value the raw value
+ * @return a valid label value, or an empty string if nothing usable is left
+ */
+ public static String generateK8sLabelValue(String value) {
+ if (StringUtils.isBlank(value)) {
+ return "";
+ }
+ String result = value.toLowerCase().replaceAll("[^a-z0-9.-]", "");
+ if (result.length() > MAX_LABEL_VALUE_LENGTH) {
+ result = result.substring(0, MAX_LABEL_VALUE_LENGTH);
+ }
+ return result.replaceAll("^[^a-z0-9]+|[^a-z0-9]+$", "");
+ }
}
diff --git
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
index 4cbf11456b..f53edcef1e 100644
---
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
+++
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
@@ -29,6 +29,7 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.time.Duration;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@@ -125,7 +126,7 @@ class K8sRemoteInterpreterProcessTest {
assertEquals("12321:12321" ,
p.get("zeppelin.k8s.interpreter.rpc.portRange"));
assertEquals("zeppelin.server.service" ,
p.get("zeppelin.k8s.server.rpc.service"));
assertEquals(12320 , p.get("zeppelin.k8s.server.rpc.portRange"));
- assertEquals("null", p.get("zeppelin.k8s.interpreter.user"));
+ assertNull(p.get("zeppelin.k8s.interpreter.user"));
assertEquals("v1", p.get("my.key1"));
assertEquals("V1", envs.get("MY_ENV1"));
@@ -230,7 +231,7 @@ class K8sRemoteInterpreterProcessTest {
// then
assertEquals("spark-container:1.0",
p.get("zeppelin.k8s.spark.container.image"));
assertEquals(String.format("//4040-%s.%s", intp.getPodName(), "mydomain"),
p.get("zeppelin.spark.uiWebUrl"));
- assertEquals("mytestUser", p.get("zeppelin.k8s.interpreter.user"));
+ assertEquals("mytestuser", p.get("zeppelin.k8s.interpreter.user"));
envs = (HashMap<String, String>) p.get("zeppelin.k8s.envs");
assertTrue( envs.containsKey("SPARK_HOME"));
@@ -599,4 +600,57 @@ class K8sRemoteInterpreterProcessTest {
}
}
+ private K8sRemoteInterpreterProcess createProcessForLabelTest() {
+ return new K8sRemoteInterpreterProcess(
+ client,
+ "default",
+ new File(".skip"),
+ "interpreter-container:1.0",
+ "shared_process",
+ "sh",
+ "shell",
+ new Properties(),
+ new HashMap<>(),
+ "zeppelin.server.service",
+ 12320,
+ false,
+ "spark-container:1.0",
+ 10,
+ 10,
+ false,
+ false);
+ }
+
+ private String renderSpec(String principal) throws IOException {
+ K8sRemoteInterpreterProcess intp = createProcessForLabelTest();
+ K8sSpecTemplate template = new K8sSpecTemplate();
+ template.loadProperties(intp.getTemplateBindings(principal));
+ URL url = Thread.currentThread().getContextClassLoader()
+ .getResource("k8s-specs/interpreter-spec.yaml");
+ return template.render(new File(url.getPath()));
+ }
+
+ @Test
+ void testUserLabelIsSanitizedInRenderedSpec() throws IOException {
+ String spec = renderSpec("Firstname Lastname@Corp");
+
+ String userLine = Arrays.stream(spec.split("\n"))
+ .map(String::trim)
+ .filter(l -> l.startsWith("user:"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("user label not rendered:\n" +
spec));
+ String value = userLine.substring("user:".length()).trim();
+ assertEquals("firstnamelastnamecorp", value);
+ assertTrue(value.length() <= 63);
+ assertTrue(value.matches("(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?"));
+ }
+
+ @Test
+ void testUserLabelOmittedForBlankPrincipal() throws IOException {
+ for (String principal : new String[] {null, "", " ", "!@#"}) {
+ String spec = renderSpec(principal);
+ assertFalse(spec.contains("user:"), "principal=" + principal + "\n" +
spec);
+ assertTrue(spec.contains("interpreterSettingName:"));
+ }
+ }
}
diff --git
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sUtilsTest.java
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sUtilsTest.java
index 40dcce0f25..39be5878d0 100644
---
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sUtilsTest.java
+++
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sUtilsTest.java
@@ -69,4 +69,31 @@ class K8sUtilsTest {
assertEquals(253 - "zeppelin".length() ,
K8sUtils.generateK8sName(RandomStringUtils.randomAlphabetic(260),
true).length());
}
+
+ @Test
+ void testGenerateK8sLabelValue() {
+ assertEquals("firstnamelastname",
K8sUtils.generateK8sLabelValue("Firstname Lastname"));
+ assertEquals("alicecorp.com",
K8sUtils.generateK8sLabelValue("[email protected]"));
+ assertEquals("test", K8sUtils.generateK8sLabelValue(" test "));
+ // leading and trailing non alphanumeric characters are dropped
+ assertEquals("test", K8sUtils.generateK8sLabelValue("-.test.-"));
+ assertEquals("a-b", K8sUtils.generateK8sLabelValue("-a-b-"));
+ // nothing usable left
+ assertEquals("", K8sUtils.generateK8sLabelValue(""));
+ assertEquals("", K8sUtils.generateK8sLabelValue(" "));
+ assertEquals("", K8sUtils.generateK8sLabelValue("!@#"));
+ assertEquals("", K8sUtils.generateK8sLabelValue(null));
+ }
+
+ @Test
+ void testGenerateK8sLabelValueTruncatesTo63() {
+ String longValue = RandomStringUtils.randomAlphabetic(100);
+ String result = K8sUtils.generateK8sLabelValue(longValue);
+ assertEquals(63, result.length());
+ assertEquals(longValue.toLowerCase().substring(0, 63), result);
+
+ // a separator at the cut position must not remain at the end
+ String cutOnSeparator =
RandomStringUtils.randomAlphabetic(62).toLowerCase() + "-tail";
+ assertEquals(cutOnSeparator.substring(0, 62),
K8sUtils.generateK8sLabelValue(cutOnSeparator));
+ }
}
diff --git
a/zeppelin-plugins/launcher/k8s-standard/src/test/resources/k8s-specs/interpreter-spec.yaml
b/zeppelin-plugins/launcher/k8s-standard/src/test/resources/k8s-specs/interpreter-spec.yaml
index 5ca0637060..d090c4f089 100644
---
a/zeppelin-plugins/launcher/k8s-standard/src/test/resources/k8s-specs/interpreter-spec.yaml
+++
b/zeppelin-plugins/launcher/k8s-standard/src/test/resources/k8s-specs/interpreter-spec.yaml
@@ -23,7 +23,9 @@ metadata:
app: {{zeppelin.k8s.interpreter.pod.name}}
interpreterGroupId: {{zeppelin.k8s.interpreter.group.id}}
interpreterSettingName: {{zeppelin.k8s.interpreter.setting.name}}
+ {% if zeppelin.k8s.interpreter.user %}
user: {{ zeppelin.k8s.interpreter.user }}
+ {% endif %}
{% if zeppelin.k8s.server.uid is defined %}
ownerReferences:
- apiVersion: v1