From 719c3b2ed6173ab6b07ded2440fd14eb255c4f33 Mon Sep 17 00:00:00 2001 From: Sadananda Aithal <111732128+saithal-confluent@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:12:35 +0530 Subject: [PATCH 1/2] Add advertisedPlaintextPort to DruidNode for sidecar proxy support Allow Druid nodes to advertise a different plaintext port (e.g. an envoy sidecar port) for peer discovery while binding Jetty to the actual service port. This enables east-west mTLS via a sidecar proxy without changing Druid's bind configuration. The new `advertisedPlaintextPort` JSON property on DruidNode controls the port advertised during peer discovery. When unset (or <= 0), it falls back to `plaintextPort` preserving existing behaviour. --- .../org/apache/druid/rpc/ServiceLocation.java | 2 +- .../org/apache/druid/server/DruidNode.java | 48 ++++++-- .../apache/druid/server/DruidNodeTest.java | 112 ++++++++++++++++++ 3 files changed, 153 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/apache/druid/rpc/ServiceLocation.java b/server/src/main/java/org/apache/druid/rpc/ServiceLocation.java index 552e0d6e9ba3..97824aa408db 100644 --- a/server/src/main/java/org/apache/druid/rpc/ServiceLocation.java +++ b/server/src/main/java/org/apache/druid/rpc/ServiceLocation.java @@ -73,7 +73,7 @@ public ServiceLocation(final String host, final int plaintextPort, final int tls */ public static ServiceLocation fromDruidNode(final DruidNode druidNode) { - return new ServiceLocation(druidNode.getHost(), druidNode.getPlaintextPort(), druidNode.getTlsPort(), ""); + return new ServiceLocation(druidNode.getHost(), druidNode.getAdvertisedPlaintextPort(), druidNode.getTlsPort(), ""); } /** diff --git a/server/src/main/java/org/apache/druid/server/DruidNode.java b/server/src/main/java/org/apache/druid/server/DruidNode.java index 19252fde755c..03791d3d8c15 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -100,6 +100,10 @@ public class DruidNode @JsonProperty private Map labels; + @JsonProperty + @Max(0xffff) + private int advertisedPlaintextPort = -1; + public DruidNode( String serviceName, String host, @@ -110,7 +114,21 @@ public DruidNode( boolean enableTlsPort ) { - this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, enablePlaintextPort, enableTlsPort, null); + this(serviceName, host, bindOnHost, plaintextPort, null, tlsPort, enablePlaintextPort, enableTlsPort, null, null); + } + + public DruidNode( + String serviceName, + String host, + boolean bindOnHost, + Integer plaintextPort, + Integer port, + Integer tlsPort, + Boolean enablePlaintextPort, + boolean enableTlsPort + ) + { + this(serviceName, host, bindOnHost, plaintextPort, port, tlsPort, enablePlaintextPort, enableTlsPort, null, null); } /** @@ -139,7 +157,8 @@ public DruidNode( @JacksonInject(useInput = OptBoolean.TRUE) @Named("tlsServicePort") @JsonProperty("tlsPort") Integer tlsPort, @JsonProperty("enablePlaintextPort") Boolean enablePlaintextPort, @JsonProperty("enableTlsPort") boolean enableTlsPort, - @JsonProperty("labels") @Nullable Map labels + @JsonProperty("labels") @Nullable Map labels, + @JsonProperty("advertisedPlaintextPort") Integer advertisedPlaintextPort ) { init( @@ -150,7 +169,8 @@ public DruidNode( tlsPort, enablePlaintextPort == null || enablePlaintextPort.booleanValue(), enableTlsPort, - labels + labels, + advertisedPlaintextPort ); } @@ -162,7 +182,8 @@ private void init( Integer tlsPort, boolean enablePlaintextPort, boolean enableTlsPort, - Map labels + Map labels, + Integer advertisedPlaintextPort ) { Preconditions.checkNotNull(serviceName); @@ -210,8 +231,12 @@ private void init( } } this.plaintextPort = plainTextPort; + this.advertisedPlaintextPort = advertisedPlaintextPort != null && advertisedPlaintextPort > 0 + ? advertisedPlaintextPort + : this.plaintextPort; } else { this.plaintextPort = -1; + this.advertisedPlaintextPort = -1; } if (enableTlsPort) { this.tlsPort = tlsPort; @@ -276,9 +301,14 @@ public String getBuildRevision() return buildRevision; } + public int getAdvertisedPlaintextPort() + { + return advertisedPlaintextPort; + } + public DruidNode withService(String service) { - return new DruidNode(service, host, bindOnHost, plaintextPort, tlsPort, enablePlaintextPort, enableTlsPort); + return new DruidNode(service, host, bindOnHost, plaintextPort, null, tlsPort, enablePlaintextPort, enableTlsPort, labels, advertisedPlaintextPort); } public String getServiceScheme() @@ -292,10 +322,10 @@ public String getServiceScheme() public String getHostAndPort() { if (enablePlaintextPort) { - if (plaintextPort < 0) { + if (advertisedPlaintextPort < 0) { return HostAndPort.fromString(host).toString(); } else { - return HostAndPort.fromParts(host, plaintextPort).toString(); + return HostAndPort.fromParts(host, advertisedPlaintextPort).toString(); } } return null; @@ -359,6 +389,7 @@ public boolean equals(Object o) enablePlaintextPort == druidNode.enablePlaintextPort && tlsPort == druidNode.tlsPort && enableTlsPort == druidNode.enableTlsPort && + advertisedPlaintextPort == druidNode.advertisedPlaintextPort && Objects.equals(serviceName, druidNode.serviceName) && Objects.equals(host, druidNode.host) && Objects.equals(labels, druidNode.labels); @@ -367,7 +398,7 @@ public boolean equals(Object o) @Override public int hashCode() { - return Objects.hash(serviceName, host, port, plaintextPort, enablePlaintextPort, tlsPort, enableTlsPort, labels); + return Objects.hash(serviceName, host, port, plaintextPort, enablePlaintextPort, tlsPort, enableTlsPort, labels, advertisedPlaintextPort); } @Override @@ -379,6 +410,7 @@ public String toString() ", bindOnHost=" + bindOnHost + ", port=" + port + ", plaintextPort=" + plaintextPort + + ", advertisedPlaintextPort=" + advertisedPlaintextPort + ", enablePlaintextPort=" + enablePlaintextPort + ", tlsPort=" + tlsPort + ", enableTlsPort=" + enableTlsPort + diff --git a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java index 9b7762904d17..51b9266065f5 100644 --- a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java +++ b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java @@ -188,6 +188,118 @@ public void testDefaultsAndSanity() Assert.assertEquals(-1, node.getTlsPort()); } + @Test + public void testAdvertisedPlaintextPort() + { + // When not set, advertisedPlaintextPort defaults to plaintextPort + DruidNode node = new DruidNode("test", "host", false, 8082, null, true, false); + Assert.assertEquals(8082, node.getPlaintextPort()); + Assert.assertEquals(8082, node.getAdvertisedPlaintextPort()); + Assert.assertEquals("host:8082", node.getHostAndPort()); + Assert.assertEquals("host:8082", node.getHostAndPortToUse()); + + // When set, advertisedPlaintextPort overrides in getHostAndPort() but not getPlaintextPort() + node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + Assert.assertEquals(8082, node.getPlaintextPort()); + Assert.assertEquals(9443, node.getAdvertisedPlaintextPort()); + Assert.assertEquals("host:9443", node.getHostAndPort()); + Assert.assertEquals("host:9443", node.getHostAndPortToUse()); + // getPortToUse() still returns the bind port + Assert.assertEquals(8082, node.getPortToUse()); + } + + @Test + public void testAdvertisedPlaintextPortWithTls() + { + // advertisedPlaintextPort with TLS enabled — getHostAndPortToUse() prefers TLS + DruidNode node = new DruidNode("test", "host", false, 8082, null, 8443, true, true, 9443); + Assert.assertEquals(8082, node.getPlaintextPort()); + Assert.assertEquals(9443, node.getAdvertisedPlaintextPort()); + Assert.assertEquals(8443, node.getTlsPort()); + Assert.assertEquals("host:9443", node.getHostAndPort()); + Assert.assertEquals("host:8443", node.getHostAndTlsPort()); + Assert.assertEquals("host:8443", node.getHostAndPortToUse()); + } + + @Test + public void testAdvertisedPlaintextPortDisabledPlaintext() + { + // When plaintext is disabled, advertisedPlaintextPort is -1 regardless + DruidNode node = new DruidNode("test", "host", false, null, null, 8443, false, true, 9443); + Assert.assertEquals(-1, node.getPlaintextPort()); + Assert.assertEquals(-1, node.getAdvertisedPlaintextPort()); + Assert.assertNull(node.getHostAndPort()); + } + + @Test + public void testAdvertisedPlaintextPortSerde() throws Exception + { + // Serialization roundtrip preserves advertisedPlaintextPort + DruidNode original = new DruidNode("service", "host", true, 8082, null, 5678, true, true, 9443); + DruidNode actual = mapper.readValue(mapper.writeValueAsString(original), DruidNode.class); + Assert.assertEquals(8082, actual.getPlaintextPort()); + Assert.assertEquals(9443, actual.getAdvertisedPlaintextPort()); + Assert.assertEquals(5678, actual.getTlsPort()); + Assert.assertEquals("host:9443", actual.getHostAndPort()); + } + + @Test + public void testAdvertisedPlaintextPortBackwardCompatDeserialization() throws Exception + { + // Old JSON without advertisedPlaintextPort — should default to plaintextPort + String json = "{\n" + + " \"service\":\"service\",\n" + + " \"host\":\"host\",\n" + + " \"plaintextPort\":8082,\n" + + " \"enablePlaintextPort\":true,\n" + + " \"enableTlsPort\":false\n" + + "}\n"; + DruidNode actual = mapper.readValue(json, DruidNode.class); + Assert.assertEquals(8082, actual.getPlaintextPort()); + Assert.assertEquals(8082, actual.getAdvertisedPlaintextPort()); + Assert.assertEquals("host:8082", actual.getHostAndPort()); + } + + @Test + public void testAdvertisedPlaintextPortDeserialization() throws Exception + { + // JSON with advertisedPlaintextPort set + String json = "{\n" + + " \"service\":\"service\",\n" + + " \"host\":\"host\",\n" + + " \"plaintextPort\":8082,\n" + + " \"advertisedPlaintextPort\":9443,\n" + + " \"enablePlaintextPort\":true,\n" + + " \"enableTlsPort\":false\n" + + "}\n"; + DruidNode actual = mapper.readValue(json, DruidNode.class); + Assert.assertEquals(8082, actual.getPlaintextPort()); + Assert.assertEquals(9443, actual.getAdvertisedPlaintextPort()); + Assert.assertEquals("host:9443", actual.getHostAndPort()); + Assert.assertEquals("host:9443", actual.getHostAndPortToUse()); + } + + @Test + public void testAdvertisedPlaintextPortWithService() + { + DruidNode node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + DruidNode copy = node.withService("other"); + Assert.assertEquals("other", copy.getServiceName()); + Assert.assertEquals(8082, copy.getPlaintextPort()); + Assert.assertEquals(9443, copy.getAdvertisedPlaintextPort()); + } + + @Test + public void testAdvertisedPlaintextPortEquality() + { + DruidNode a = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + DruidNode b = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + DruidNode c = new DruidNode("test", "host", false, 8082, null, true, false); + Assert.assertEquals(a, b); + Assert.assertNotEquals(a, c); + Assert.assertEquals(a.hashCode(), b.hashCode()); + } + @Test(expected = IllegalArgumentException.class) public void testConflictingPorts() { From b98173f14dd8b296af713d3ac8fc92ae074349e9 Mon Sep 17 00:00:00 2001 From: Sadananda Aithal Date: Fri, 19 Jun 2026 10:00:53 +0530 Subject: [PATCH 2/2] Fix DruidNode constructor source compatibility Restore the nine-argument DruidNode(..., labels) overload that was dropped when advertisedPlaintextPort was added to the @JsonCreator constructor, and update the new advertisedPlaintextPort tests to use the ten-argument form. Without this the server module did not compile. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/org/apache/druid/server/DruidNode.java | 15 +++++++++++++++ .../org/apache/druid/server/DruidNodeTest.java | 14 +++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/DruidNode.java b/server/src/main/java/org/apache/druid/server/DruidNode.java index 03791d3d8c15..bc59998b2fa8 100644 --- a/server/src/main/java/org/apache/druid/server/DruidNode.java +++ b/server/src/main/java/org/apache/druid/server/DruidNode.java @@ -131,6 +131,21 @@ public DruidNode( this(serviceName, host, bindOnHost, plaintextPort, port, tlsPort, enablePlaintextPort, enableTlsPort, null, null); } + public DruidNode( + String serviceName, + String host, + boolean bindOnHost, + Integer plaintextPort, + Integer port, + Integer tlsPort, + Boolean enablePlaintextPort, + boolean enableTlsPort, + Map labels + ) + { + this(serviceName, host, bindOnHost, plaintextPort, port, tlsPort, enablePlaintextPort, enableTlsPort, labels, null); + } + /** * host = null , port = null -> host = _default_, port = -1 * host = "abc:123", port = null -> host = abc, port = 123 diff --git a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java index 51b9266065f5..fc9a0f04a094 100644 --- a/server/src/test/java/org/apache/druid/server/DruidNodeTest.java +++ b/server/src/test/java/org/apache/druid/server/DruidNodeTest.java @@ -199,7 +199,7 @@ public void testAdvertisedPlaintextPort() Assert.assertEquals("host:8082", node.getHostAndPortToUse()); // When set, advertisedPlaintextPort overrides in getHostAndPort() but not getPlaintextPort() - node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, null, 9443); Assert.assertEquals(8082, node.getPlaintextPort()); Assert.assertEquals(9443, node.getAdvertisedPlaintextPort()); Assert.assertEquals("host:9443", node.getHostAndPort()); @@ -212,7 +212,7 @@ public void testAdvertisedPlaintextPort() public void testAdvertisedPlaintextPortWithTls() { // advertisedPlaintextPort with TLS enabled — getHostAndPortToUse() prefers TLS - DruidNode node = new DruidNode("test", "host", false, 8082, null, 8443, true, true, 9443); + DruidNode node = new DruidNode("test", "host", false, 8082, null, 8443, true, true, null, 9443); Assert.assertEquals(8082, node.getPlaintextPort()); Assert.assertEquals(9443, node.getAdvertisedPlaintextPort()); Assert.assertEquals(8443, node.getTlsPort()); @@ -225,7 +225,7 @@ public void testAdvertisedPlaintextPortWithTls() public void testAdvertisedPlaintextPortDisabledPlaintext() { // When plaintext is disabled, advertisedPlaintextPort is -1 regardless - DruidNode node = new DruidNode("test", "host", false, null, null, 8443, false, true, 9443); + DruidNode node = new DruidNode("test", "host", false, null, null, 8443, false, true, null, 9443); Assert.assertEquals(-1, node.getPlaintextPort()); Assert.assertEquals(-1, node.getAdvertisedPlaintextPort()); Assert.assertNull(node.getHostAndPort()); @@ -235,7 +235,7 @@ public void testAdvertisedPlaintextPortDisabledPlaintext() public void testAdvertisedPlaintextPortSerde() throws Exception { // Serialization roundtrip preserves advertisedPlaintextPort - DruidNode original = new DruidNode("service", "host", true, 8082, null, 5678, true, true, 9443); + DruidNode original = new DruidNode("service", "host", true, 8082, null, 5678, true, true, null, 9443); DruidNode actual = mapper.readValue(mapper.writeValueAsString(original), DruidNode.class); Assert.assertEquals(8082, actual.getPlaintextPort()); Assert.assertEquals(9443, actual.getAdvertisedPlaintextPort()); @@ -282,7 +282,7 @@ public void testAdvertisedPlaintextPortDeserialization() throws Exception @Test public void testAdvertisedPlaintextPortWithService() { - DruidNode node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + DruidNode node = new DruidNode("test", "host", false, 8082, null, 9443, true, false, null, 9443); DruidNode copy = node.withService("other"); Assert.assertEquals("other", copy.getServiceName()); Assert.assertEquals(8082, copy.getPlaintextPort()); @@ -292,8 +292,8 @@ public void testAdvertisedPlaintextPortWithService() @Test public void testAdvertisedPlaintextPortEquality() { - DruidNode a = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); - DruidNode b = new DruidNode("test", "host", false, 8082, null, 9443, true, false, 9443); + DruidNode a = new DruidNode("test", "host", false, 8082, null, 9443, true, false, null, 9443); + DruidNode b = new DruidNode("test", "host", false, 8082, null, 9443, true, false, null, 9443); DruidNode c = new DruidNode("test", "host", false, 8082, null, true, false); Assert.assertEquals(a, b); Assert.assertNotEquals(a, c);