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

Reply via email to