Skip to content

Commit 888e404

Browse files
committed
Kubernetes service address.
Motivation: A K8S service sometimes carries more than a single service per endpoint, when a service is resolved by name, the resolver can get multiple ports and cannot determine the port to use. Changes: Introduce a Kubernetes service builder that allows to specify the port name or the port number, so the resolver can use it to perform a precise match.
1 parent fb33f50 commit 888e404

File tree

11 files changed

+260
-28
lines changed

11 files changed

+260
-28
lines changed

src/main/asciidoc/index.adoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@ You can deal with ephemeral tokens using the {@link io.vertx.serviceresolver.kub
138138
The resolver calls the provider when it needs a fresh token. The token is cached by the resolver until it is detetected
139139
stale by the resolver (upon a `401` server response code).
140140

141+
==== Matching specific service ports
142+
143+
When a service exposes more than one port, the resolver retains only a single port, it might not be the expected port.
144+
145+
You can build a specific service address for a given service port number.
146+
147+
[source,java]
148+
----
149+
{@link examples.ServiceResolverExamples#servicePortNumberMatching}
150+
----
151+
152+
Or a given service port name.
153+
154+
[source,java]
155+
----
156+
{@link examples.ServiceResolverExamples#servicePortNameMatching}
157+
----
158+
141159
=== SRV resolver
142160

143161
The SRV resolver uses DNS SRV records to resolve and locate services.

src/main/java/examples/ServiceResolverExamples.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import io.vertx.serviceresolver.ServiceResolverClient;
2525
import io.vertx.serviceresolver.kube.KubeResolver;
2626
import io.vertx.serviceresolver.kube.KubeResolverOptions;
27+
import io.vertx.serviceresolver.kube.KubernetesServiceAddressBuilder;
28+
import io.vertx.serviceresolver.kube.impl.KubernetesServiceAddress;
2729
import io.vertx.serviceresolver.srv.SrvResolver;
2830
import io.vertx.serviceresolver.srv.SrvResolverOptions;
2931

@@ -142,6 +144,20 @@ public void configuringKubernetesResolver(String host, int port, String namespac
142144
.setWebSocketClientOptions(wsClientOptions);
143145
}
144146

147+
public void servicePortNumberMatching() {
148+
ServiceAddress serviceAddress = KubernetesServiceAddressBuilder
149+
.of("the-service")
150+
.withPortNumber(8080)
151+
.build();
152+
}
153+
154+
public void servicePortNameMatching(String portName) {
155+
ServiceAddress serviceAddress = KubernetesServiceAddressBuilder
156+
.of("the-service")
157+
.withPortName(portName)
158+
.build();
159+
}
160+
145161
public void configuringSRVResolver(Vertx vertx, String dnsServer, int dnsPort) {
146162

147163
SrvResolverOptions options = new SrvResolverOptions()

src/main/java/io/vertx/serviceresolver/ServiceAddress.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface ServiceAddress extends Address {
2929
* @param name the service name
3030
* @return the service address
3131
*/
32-
static ServiceAddress of(String name) {
32+
public static ServiceAddress of(String name) {
3333
Objects.requireNonNull(name);
3434
return new ServiceAddress() {
3535
@Override
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.vertx.serviceresolver.kube;
2+
3+
import io.vertx.serviceresolver.ServiceAddress;
4+
import io.vertx.serviceresolver.kube.impl.KubernetesServiceAddress;
5+
6+
import java.util.Objects;
7+
8+
/**
9+
* <p>Build a {@link ServiceAddress} for Kubernetes capable of distinguish a service endpoint
10+
* by their <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#endpointport-v1-core">port</a>.</p>
11+
*
12+
* <p>This is useful when dealing with pods exposing multiple ports.</p>
13+
*/
14+
public class KubernetesServiceAddressBuilder {
15+
16+
public static KubernetesServiceAddressBuilder of(String name) {
17+
return new KubernetesServiceAddressBuilder(name);
18+
}
19+
20+
private final String name;
21+
private int portNumber = 0;
22+
private String portName = null;
23+
24+
public KubernetesServiceAddressBuilder(String name) {
25+
this.name = Objects.requireNonNull(name);
26+
}
27+
28+
/**
29+
* @return the fully build service address
30+
*/
31+
public ServiceAddress build() {
32+
return new KubernetesServiceAddress(name, portNumber, portName);
33+
}
34+
35+
/**
36+
* Specify a port {@code number}, otherwise any port will be matched.
37+
*
38+
* @param number the port number, must be positive
39+
* @return this builder
40+
*/
41+
public KubernetesServiceAddressBuilder withPortNumber(int number) {
42+
if (number < 0) {
43+
throw new IllegalArgumentException("Port number must be a positive integer");
44+
}
45+
this.portNumber = number;
46+
return this;
47+
}
48+
49+
/**
50+
* Specify a TCP port {@code name}, otherwise any port will be matched.
51+
*
52+
* @param name the port name
53+
* @return this builder
54+
*/
55+
public KubernetesServiceAddressBuilder withPortName(String name) {
56+
this.portName = name;
57+
return this;
58+
}
59+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"apiVersion": "v1",
3+
"kind": "List",
4+
"items": [
5+
{
6+
"apiVersion": "v1",
7+
"kind": "Endpoints",
8+
"metadata": {
9+
"creationTimestamp": "2025-10-08T14:31:20.049213Z",
10+
"generation": 1,
11+
"labels": {
12+
"app.kubernetes.io/name": "svc",
13+
"app.kubernetes.io/version": "1.0"
14+
},
15+
"name": "svc",
16+
"namespace": "test",
17+
"resourceVersion": "3",
18+
"uid": "f28931fe-942d-4391-90c9-31a4e03971da"
19+
},
20+
"subsets": [
21+
{
22+
"addresses": [
23+
{
24+
"ip": "0.0.0.0",
25+
"targetRef": {
26+
"kind": "Pod",
27+
"name": "svc-0000-8080-8081",
28+
"namespace": "test",
29+
"uid": "5566d0e7-996b-43d8-b8d9-5950fa5ffc34"
30+
}
31+
}
32+
],
33+
"ports": [
34+
{
35+
"port": 8080,
36+
"protocol": "TCP"
37+
},
38+
{
39+
"port": 8081,
40+
"protocol": "TCP"
41+
}
42+
]
43+
}
44+
]
45+
}
46+
],
47+
"metadata": {
48+
"resourceVersion": "3",
49+
"selfLink": ""
50+
}
51+
}

src/main/java/io/vertx/serviceresolver/kube/impl/KubeResolverImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public Future<KubeServiceState<B>> resolve(ServiceAddress address, EndpointBuild
144144
Future<EndpoinsRequest<JsonObject>> endpointsFuture = requestEndpoints(token, 0);
145145
return endpointsFuture
146146
.compose(endpointsRequest -> {
147-
KubeServiceState<B> state = new KubeServiceState<>(builder, address.name());
147+
KubeServiceState<B> state = new KubeServiceState<>(builder, address, address.name());
148148
JsonObject response = endpointsRequest.payload;
149149
String resourceVersion = response.getJsonObject("metadata").getString("resourceVersion");
150150
JsonArray items = response.getJsonArray("items");

src/main/java/io/vertx/serviceresolver/kube/impl/KubeServiceState.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,26 @@
1515
import io.vertx.core.json.JsonObject;
1616
import io.vertx.core.net.SocketAddress;
1717
import io.vertx.core.spi.endpoint.EndpointBuilder;
18+
import io.vertx.serviceresolver.ServiceAddress;
1819

1920
import java.util.ArrayList;
2021
import java.util.List;
2122
import java.util.concurrent.atomic.AtomicReference;
2223

2324
class KubeServiceState<B> {
2425

26+
final ServiceAddress address;
2527
final String name;
2628
final EndpointBuilder<B, SocketAddress> endpointsBuilder;
2729
boolean disposed;
2830
WebSocket webSocket;
2931
AtomicReference<B> endpoints = new AtomicReference<>();
3032
volatile boolean valid;
3133

32-
KubeServiceState(EndpointBuilder<B, SocketAddress> endpointsBuilder, String name) {
34+
KubeServiceState(EndpointBuilder<B, SocketAddress> endpointsBuilder, ServiceAddress address, String name) {
3335
this.endpointsBuilder = endpointsBuilder;
3436
this.name = name;
37+
this.address = address;
3538
this.valid = true;
3639
}
3740

@@ -68,11 +71,22 @@ void updateEndpoints(JsonObject item) {
6871
}
6972
for (int k = 0;k < ports.size();k++) {
7073
JsonObject port = ports.getJsonObject(k);
71-
int podPort = port.getInteger("port");
74+
int portNumber = port.getInteger("port");
75+
String portName = port.getString("name");
76+
if (address instanceof KubernetesServiceAddress) {
77+
KubernetesServiceAddress kubernetesAddress = (KubernetesServiceAddress) address;
78+
if (kubernetesAddress.portNumber > 0 && kubernetesAddress.portNumber != portNumber) {
79+
continue;
80+
}
81+
if (kubernetesAddress.portName != null && !kubernetesAddress.portName.equals(portName)) {
82+
continue;
83+
}
84+
}
7285
for (String podIp : podIps) {
73-
SocketAddress podAddress = SocketAddress.inetSocketAddress(podPort, podIp);
74-
builder = builder.addServer(podAddress, podIp + "-" + podPort);
86+
SocketAddress podAddress = SocketAddress.inetSocketAddress(portNumber, podIp);
87+
builder = builder.addServer(podAddress, podIp + "-" + port);
7588
}
89+
break;
7690
}
7791
}
7892
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.vertx.serviceresolver.kube.impl;
2+
3+
import io.vertx.serviceresolver.ServiceAddress;
4+
5+
public class KubernetesServiceAddress implements ServiceAddress {
6+
7+
final String name;
8+
final int portNumber;
9+
final String portName;
10+
11+
public KubernetesServiceAddress(String name, int portNumber, String portName) {
12+
this.name = name;
13+
this.portNumber = portNumber;
14+
this.portName = portName;
15+
}
16+
17+
@Override
18+
public String name() {
19+
return name;
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.vertx.tests.kube;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
6+
public class KubeEndpoint {
7+
8+
final String ip;
9+
final Map<Integer, String> ports;
10+
11+
public KubeEndpoint(String ip, int port) {
12+
this.ip = ip;
13+
this.ports = Collections.singletonMap(port, null);
14+
}
15+
16+
public KubeEndpoint(String ip, Map<Integer, String> ports) {
17+
this.ip = ip;
18+
this.ports = ports;
19+
}
20+
}

src/test/java/io/vertx/tests/kube/KubeServiceResolverTestBase.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.vertx.serviceresolver.ServiceAddress;
1010
import io.vertx.serviceresolver.kube.KubeResolver;
1111
import io.vertx.serviceresolver.kube.KubeResolverOptions;
12+
import io.vertx.serviceresolver.kube.KubernetesServiceAddressBuilder;
1213
import io.vertx.tests.HttpProxy;
1314
import io.vertx.tests.ServiceResolverTestBase;
1415
import org.junit.Ignore;
@@ -95,6 +96,22 @@ public void testNoPods(TestContext should) throws Exception {
9596
}
9697
}
9798

99+
@Test
100+
public void testSome(TestContext should) throws Exception {
101+
Handler<HttpServerRequest> server = req -> {
102+
req.response().end("" + req.localAddress().port());
103+
};
104+
List<SocketAddress> pods = startPods(2, server);
105+
ServiceAddress service = ServiceAddress.of("svc");
106+
kubernetesMocking.buildAndRegisterBackendPod(service, kubernetesMocking.defaultNamespace(), KubeOp.CREATE, pods);
107+
KubeEndpoint kubeEndpoint = new KubeEndpoint(pods.get(0).host(), Map.of(pods.get(0).port(), "p1", pods.get(1).port(), "p2"));
108+
kubernetesMocking.buildAndRegisterKubernetesService(service.name(), kubernetesMocking.defaultNamespace(), KubeOp.CREATE, kubeEndpoint);
109+
should.assertEquals("8080", get(KubernetesServiceAddressBuilder.of("svc").withPortName("p1").build()).toString());
110+
should.assertEquals("8081", get(KubernetesServiceAddressBuilder.of("svc").withPortName("p2").build()).toString());
111+
should.assertEquals("8080", get(KubernetesServiceAddressBuilder.of("svc").withPortNumber(8080).build()).toString());
112+
should.assertEquals("8081", get(KubernetesServiceAddressBuilder.of("svc").withPortNumber(8081).build()).toString());
113+
}
114+
98115
@Test
99116
public void testUpdate() throws Exception {
100117
Handler<HttpServerRequest> server = req -> {

0 commit comments

Comments
 (0)