diff --git a/README.md b/README.md
index 0e9390a78..e47b11559 100644
--- a/README.md
+++ b/README.md
@@ -651,6 +651,16 @@ this.router.get("/secured")
* authZ::authorize: custom AuthZ(authorization) provider.
* secure.helloWorld(rc): actual http endpoint (Rest layer).
+### Service-discovery/stork
+
+Verifies Stork integration in order to provide service discovering and round-robin load balancing between services
+
+`StorkServiceDiscoveryIT` scenario deploys four services:
+* Pung: is a simple endpoint that returns "pung" as a string
+* Pong: is a simple endpoint that returns "pong" as a string
+* PongReplica: is a "Pong service" replica, that is deployed in another physical service
+* Ping: is the main client microservice that will use `pung` and `pong` (Pong and PongReplica) services. The service
+discovery will be done by Stork, and the request dispatching between "pong" services is going to be done by Stork load balancer.
### `monitoring/microprofile`
diff --git a/pom.xml b/pom.xml
index ade1a3187..866c37f6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -349,6 +349,7 @@
docker-build
javaee-like-getting-started
scaling
+ service-discovery/stork
lifecycle-application
external-applications
scheduling/quartz
diff --git a/service-discovery/stork/pom.xml b/service-discovery/stork/pom.xml
new file mode 100644
index 000000000..729075597
--- /dev/null
+++ b/service-discovery/stork/pom.xml
@@ -0,0 +1,67 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ stork
+ jar
+ Quarkus QE TS: Service-discovery: Stork
+
+
+ io.smallrye.stork
+ stork-service-discovery-consul
+
+
+ io.smallrye.reactive
+ smallrye-mutiny-vertx-consul-client
+
+
+ io.smallrye.stork
+ stork-service-discovery-kubernetes
+
+
+ io.quarkus
+ quarkus-reactive-routes
+
+
+ io.quarkus
+ quarkus-rest-client-reactive
+
+
+ io.quarkus.qe
+ quarkus-test-service-consul
+ test
+
+
+
+
+
+ skip-tests-on-windows
+
+
+ windows
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPongProxy.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPongProxy.java
new file mode 100644
index 000000000..e222e6518
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPongProxy.java
@@ -0,0 +1,20 @@
+package io.quarkus.ts.stork;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.jboss.resteasy.reactive.RestResponse;
+
+import io.smallrye.mutiny.Uni;
+
+@Path("/pong")
+@RegisterRestClient(baseUri = "stork://pong")
+public interface MyBackendPongProxy {
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/")
+ Uni> get();
+}
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPungProxy.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPungProxy.java
new file mode 100644
index 000000000..83af3f220
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/MyBackendPungProxy.java
@@ -0,0 +1,19 @@
+package io.quarkus.ts.stork;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+import io.smallrye.mutiny.Uni;
+
+@Path("/pung")
+@RegisterRestClient(baseUri = "stork://pung")
+public interface MyBackendPungProxy {
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/")
+ Uni get();
+}
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PingResource.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PingResource.java
new file mode 100644
index 000000000..4c6c0e35b
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PingResource.java
@@ -0,0 +1,39 @@
+package io.quarkus.ts.stork;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+
+import io.quarkus.vertx.web.Route;
+import io.quarkus.vertx.web.RouteBase;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@RouteBase(path = "/ping", produces = MediaType.TEXT_PLAIN)
+public class PingResource {
+
+ public static final String PING_PREFIX = "ping-";
+ public static final String HEADER_ID = "x-id";
+
+ @RestClient
+ MyBackendPongProxy pongService;
+
+ @RestClient
+ MyBackendPungProxy pungService;
+
+ @Route(methods = Route.HttpMethod.GET, path = "/pung")
+ public Uni pung() {
+ return pungService.get()
+ .onFailure().transform(error -> new WebApplicationException(error.getMessage()))
+ .map(resp -> PING_PREFIX + resp);
+ }
+
+ @Route(methods = Route.HttpMethod.GET, path = "/pong")
+ public void pong(RoutingContext context) {
+ pongService.get().onFailure().transform(error -> new WebApplicationException(error.getMessage())).subscribe()
+ .with(resp -> context.response()
+ .putHeader(HEADER_ID, resp.getHeaderString(HEADER_ID))
+ .end(PING_PREFIX + resp.getEntity()));
+ }
+}
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongReplicaResource.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongReplicaResource.java
new file mode 100644
index 000000000..9b7fa91d0
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongReplicaResource.java
@@ -0,0 +1,50 @@
+package io.quarkus.ts.stork;
+
+import static io.quarkus.ts.stork.PongResource.PONG_SERVICE_NAME;
+
+import javax.enterprise.event.Observes;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.runtime.StartupEvent;
+import io.quarkus.vertx.web.Route;
+import io.quarkus.vertx.web.RouteBase;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.consul.ConsulClientOptions;
+import io.vertx.ext.consul.ServiceOptions;
+import io.vertx.mutiny.core.Vertx;
+import io.vertx.mutiny.ext.consul.ConsulClient;
+
+@RouteBase(path = "/pong", produces = MediaType.TEXT_PLAIN)
+public class PongReplicaResource {
+
+ private static final String DEFAULT_PONG_REPLICA_RESPONSE = "pongReplica";
+
+ @ConfigProperty(name = "stork.pong-replica.service-discovery.consul-host", defaultValue = "localhost")
+ String host;
+ @ConfigProperty(name = "stork.pong-replica.service-discovery.consul-port", defaultValue = "8500")
+ String port;
+ @ConfigProperty(name = "pong-replica-service-port", defaultValue = "8080")
+ String pongPort;
+ @ConfigProperty(name = "pong-replica-service-host", defaultValue = "localhost")
+ String pongHost;
+ @ConfigProperty(name = "stork.pong-replica.service-discovery", defaultValue = "consul")
+ String serviceDiscoveryType;
+
+ public void init(@Observes StartupEvent ev, Vertx vertx) {
+ if (serviceDiscoveryType.equalsIgnoreCase("consul")) {
+ ConsulClient client = ConsulClient.create(vertx,
+ new ConsulClientOptions().setHost(host).setPort(Integer.parseInt(port)));
+
+ client.registerServiceAndAwait(
+ new ServiceOptions().setPort(Integer.parseInt(pongPort)).setAddress(pongHost).setName(PONG_SERVICE_NAME)
+ .setId("pongReplica"));
+ }
+ }
+
+ @Route(path = "/", methods = Route.HttpMethod.GET)
+ public Uni pong() {
+ return Uni.createFrom().item(DEFAULT_PONG_REPLICA_RESPONSE);
+ }
+}
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongResource.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongResource.java
new file mode 100644
index 000000000..67b6d6076
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PongResource.java
@@ -0,0 +1,54 @@
+package io.quarkus.ts.stork;
+
+import java.util.UUID;
+
+import javax.enterprise.event.Observes;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.runtime.StartupEvent;
+import io.quarkus.vertx.web.Route;
+import io.quarkus.vertx.web.RouteBase;
+import io.vertx.ext.consul.ConsulClientOptions;
+import io.vertx.ext.consul.ServiceOptions;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.mutiny.core.Vertx;
+import io.vertx.mutiny.ext.consul.ConsulClient;
+
+@RouteBase(path = "/pong", produces = MediaType.TEXT_PLAIN)
+public class PongResource {
+
+ public static final String PONG_SERVICE_NAME = "pong";
+ private static final String DEFAULT_PONG_RESPONSE = "pong";
+ private static final String HEADER_ID = "x-id";
+ private String instanceUniqueId;
+
+ @ConfigProperty(name = "stork.pong.service-discovery.consul-host", defaultValue = "localhost")
+ String host;
+ @ConfigProperty(name = "stork.pong.service-discovery.consul-port", defaultValue = "8500")
+ String port;
+ @ConfigProperty(name = "pong-service-port", defaultValue = "8080")
+ String pongPort;
+ @ConfigProperty(name = "pong-service-host", defaultValue = "localhost")
+ String pongHost;
+ @ConfigProperty(name = "stork.pong.service-discovery", defaultValue = "consul")
+ String serviceDiscoveryType;
+
+ public void init(@Observes StartupEvent ev, Vertx vertx) {
+ instanceUniqueId = UUID.randomUUID().toString();
+ if (serviceDiscoveryType.equalsIgnoreCase("consul")) {
+ ConsulClient client = ConsulClient.create(vertx,
+ new ConsulClientOptions().setHost(host).setPort(Integer.parseInt(port)));
+
+ client.registerServiceAndAwait(
+ new ServiceOptions().setPort(Integer.parseInt(pongPort)).setAddress(pongHost).setName(PONG_SERVICE_NAME)
+ .setId("pong"));
+ }
+ }
+
+ @Route(path = "/", methods = Route.HttpMethod.GET)
+ public void pong(final RoutingContext context) {
+ context.response().putHeader(HEADER_ID, instanceUniqueId).end(DEFAULT_PONG_RESPONSE);
+ }
+}
diff --git a/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PungResource.java b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PungResource.java
new file mode 100644
index 000000000..60f588be2
--- /dev/null
+++ b/service-discovery/stork/src/main/java/io/quarkus/ts/stork/PungResource.java
@@ -0,0 +1,46 @@
+package io.quarkus.ts.stork;
+
+import javax.enterprise.event.Observes;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.runtime.StartupEvent;
+import io.quarkus.vertx.web.Route;
+import io.quarkus.vertx.web.RouteBase;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.consul.ConsulClientOptions;
+import io.vertx.ext.consul.ServiceOptions;
+import io.vertx.mutiny.core.Vertx;
+import io.vertx.mutiny.ext.consul.ConsulClient;
+
+@RouteBase(path = "/pung", produces = MediaType.TEXT_PLAIN)
+public class PungResource {
+
+ @ConfigProperty(name = "stork.pung.service-discovery.consul-host", defaultValue = "localhost")
+ String host;
+ @ConfigProperty(name = "stork.pung.service-discovery.consul-port", defaultValue = "8500")
+ String port;
+ @ConfigProperty(name = "pung-service-port", defaultValue = "8080")
+ String pungPort;
+ @ConfigProperty(name = "pung-service-host", defaultValue = "localhost")
+ String pungHost;
+ @ConfigProperty(name = "stork.pung.service-discovery", defaultValue = "consul")
+ String serviceDiscoveryType;
+
+ public void init(@Observes StartupEvent ev, Vertx vertx) {
+ if (serviceDiscoveryType.equalsIgnoreCase("consul")) {
+ ConsulClient client = ConsulClient.create(vertx,
+ new ConsulClientOptions().setHost(host).setPort(Integer.parseInt(port)));
+
+ client.registerServiceAndAwait(
+ new ServiceOptions().setPort(Integer.parseInt(pungPort)).setAddress(pungHost).setName("pung")
+ .setId("pung"));
+ }
+ }
+
+ @Route(path = "/", methods = Route.HttpMethod.GET)
+ public Uni pung() {
+ return Uni.createFrom().item("pung");
+ }
+}
diff --git a/service-discovery/stork/src/main/resources/application.properties b/service-discovery/stork/src/main/resources/application.properties
new file mode 100644
index 000000000..2a708270c
--- /dev/null
+++ b/service-discovery/stork/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+# application properties should be here
+# TODO https://github.com/quarkusio/quarkus/issues/24444
+quarkus.native.additional-build-args=--allow-incomplete-classpath, --initialize-at-run-time=io.fabric8.kubernetes.client.internal.CertUtils, --enable-url-protocols=https
diff --git a/service-discovery/stork/src/test/java/io/quarkus/ts/stork/OpenShiftStorkServiceDiscoveryIT.java b/service-discovery/stork/src/test/java/io/quarkus/ts/stork/OpenShiftStorkServiceDiscoveryIT.java
new file mode 100644
index 000000000..a670edd72
--- /dev/null
+++ b/service-discovery/stork/src/test/java/io/quarkus/ts/stork/OpenShiftStorkServiceDiscoveryIT.java
@@ -0,0 +1,111 @@
+package io.quarkus.ts.stork;
+
+import static io.quarkus.ts.stork.PingResource.HEADER_ID;
+import static io.quarkus.ts.stork.PingResource.PING_PREFIX;
+import static java.util.regex.Pattern.quote;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.bootstrap.inject.OpenShiftClient;
+import io.quarkus.test.scenarios.OpenShiftScenario;
+import io.quarkus.test.services.QuarkusApplication;
+import io.quarkus.test.utils.FileUtils;
+import io.restassured.response.ValidatableResponse;
+
+@OpenShiftScenario
+public class OpenShiftStorkServiceDiscoveryIT {
+
+ private static final String CLUSTER_ROLE_FILE_NAME = "cluster-role.yaml";
+ private static final String RBAC_FILE_NAME = "fabric8-rbac.yaml";
+ private static final int PONG_INSTANCES_AMOUNT = 2; // we need at least two instances in order to verify stork LB
+
+ @Inject
+ static OpenShiftClient openshift;
+
+ @QuarkusApplication(classes = PungResource.class)
+ static RestService pung = new RestService()
+ .withProperty("stork.pung.service-discovery", "kubernetes")
+ .withProperty("stork.pung.service-discovery.k8s-namespace", "all");
+
+ @QuarkusApplication(classes = PongResource.class)
+ static RestService pong = new RestService()
+ .onPostStart(app -> openshift.scaleTo(app, PONG_INSTANCES_AMOUNT))
+ .withProperty("stork.pong.service-discovery", "kubernetes")
+ .withProperty("stork.pong.service-discovery.k8s-namespace", "all");
+
+ @QuarkusApplication(classes = { PingResource.class, MyBackendPungProxy.class, MyBackendPongProxy.class })
+ static RestService ping = new RestService().onPreStart(app -> setupClusterRoles())
+ .withProperty("stork.pong.service-discovery", "kubernetes")
+ .withProperty("stork.pong.service-discovery.k8s-namespace", "all")
+ .withProperty("stork.pung.service-discovery", "kubernetes")
+ .withProperty("stork.pung.service-discovery.k8s-namespace", "all");
+
+ @AfterAll
+ public static void tearDown() {
+ openshift.delete(Paths.get(new File("target/test-classes/" + CLUSTER_ROLE_FILE_NAME).toURI()));
+ openshift.delete(Paths.get(new File("target/test-classes/" + RBAC_FILE_NAME).toURI()));
+ }
+
+ @Test
+ public void invokeServiceByName() {
+ String response = makePingCall("pung").extract().body().asString();
+ assertThat("Service discovery by name fail.", PING_PREFIX + "pung", is(response));
+ }
+
+ @Test
+ public void storkLoadBalancerByRoundRobin() {
+ Map uniqueResp = new HashMap<>();
+ final int requestAmount = 10;
+ final int roundRobinError = (requestAmount / PONG_INSTANCES_AMOUNT) - 1;
+ for (int i = 0; i < requestAmount; i++) {
+ String pongInstanceId = makePingCall("pong").extract().header(HEADER_ID);
+ if (uniqueResp.containsKey(pongInstanceId)) {
+ uniqueResp.put(pongInstanceId, uniqueResp.get(pongInstanceId) + 1);
+ } else {
+ uniqueResp.put(pongInstanceId, 1);
+ }
+ }
+
+ Assertions.assertEquals(uniqueResp.size(), PONG_INSTANCES_AMOUNT,
+ "Only " + PONG_INSTANCES_AMOUNT + " services should response");
+
+ for (Map.Entry pod : uniqueResp.entrySet()) {
+ Assertions.assertTrue(uniqueResp.get(pod.getKey()) >= roundRobinError,
+ "Request load is not distributed following a round-robin distribution");
+ }
+ }
+
+ private ValidatableResponse makePingCall(String subPath) {
+ return ping
+ .given()
+ .get("/ping/" + subPath).then()
+ .statusCode(HttpStatus.SC_OK);
+ }
+
+ /**
+ * setup `stork-service-discovery-kubernetes` - roles and roles bindings (required)
+ */
+ private static void setupClusterRoles() {
+ String namespace = openshift.project();
+ String clusterRoleContent = FileUtils.loadFile(new File("target/test-classes/" + CLUSTER_ROLE_FILE_NAME))
+ .replaceAll(quote("${NAMESPACE}"), namespace);
+ openshift.apply(FileUtils.copyContentTo(clusterRoleContent,
+ new File("target/test-classes/" + CLUSTER_ROLE_FILE_NAME).toPath()));
+ String contentRBAC = FileUtils.loadFile(new File("target/test-classes/" + RBAC_FILE_NAME))
+ .replaceAll(quote("${NAMESPACE}"), namespace);
+ openshift.apply(FileUtils.copyContentTo(contentRBAC, new File("target/test-classes/" + RBAC_FILE_NAME).toPath()));
+ }
+}
diff --git a/service-discovery/stork/src/test/java/io/quarkus/ts/stork/StorkServiceDiscoveryIT.java b/service-discovery/stork/src/test/java/io/quarkus/ts/stork/StorkServiceDiscoveryIT.java
new file mode 100644
index 000000000..9e8717a80
--- /dev/null
+++ b/service-discovery/stork/src/test/java/io/quarkus/ts/stork/StorkServiceDiscoveryIT.java
@@ -0,0 +1,133 @@
+package io.quarkus.ts.stork;
+
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.ConsulService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+import junit.framework.AssertionFailedError;
+
+@QuarkusScenario
+public class StorkServiceDiscoveryIT {
+
+ @Container(image = "${consul.image}", expectedLog = "Synced node info", port = 8500)
+ static ConsulService consul = new ConsulService();
+
+ private static final String PREFIX = "ping-";
+ private static final String DEFAULT_PONG_REPLICA_RESPONSE = "pongReplica";
+ private static final String DEFAULT_PONG_RESPONSE = "pong";
+ private static final String PUNG_PORT = getAvailablePort();
+ private static final String PONG_PORT = getAvailablePort();
+ private static final String PONG_REPLICA_PORT = getAvailablePort();
+
+ @QuarkusApplication(classes = PungResource.class)
+ static RestService pungService = new RestService()
+ .withProperty("quarkus.http.port", PUNG_PORT)
+ .withProperty("pung-service-port", PUNG_PORT)
+ .withProperty("pung-service-host", "localhost")
+ .withProperty("stork.pung.service-discovery", "consul")
+ .withProperty("stork.pung.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pung.service-discovery.consul-host", () -> getConsultEndpoint(consul.getConsulEndpoint()));
+
+ @QuarkusApplication(classes = PongResource.class)
+ static RestService pongService = new RestService()
+ .withProperty("quarkus.http.port", PONG_PORT)
+ .withProperty("pong-service-port", PONG_PORT)
+ .withProperty("pong-service-host", "localhost")
+ .withProperty("stork.pong.service-discovery.refresh-period", "1")
+ .withProperty("stork.pong.service-discovery", "consul")
+ .withProperty("stork.pong.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pong.service-discovery.consul-host", () -> getConsultEndpoint(consul.getConsulEndpoint()));
+
+ @QuarkusApplication(classes = PongReplicaResource.class)
+ static RestService pongReplicaService = new RestService()
+ .withProperty("quarkus.http.port", PONG_REPLICA_PORT)
+ .withProperty("pong-replica-service-port", PONG_REPLICA_PORT)
+ .withProperty("pong-replica-service-host", "localhost")
+ .withProperty("stork.pong-replica.service-discovery.refresh-period", "1")
+ .withProperty("stork.pong-replica.service-discovery", "consul")
+ .withProperty("stork.pong-replica.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pong-replica.service-discovery.consul-host",
+ () -> getConsultEndpoint(consul.getConsulEndpoint()));
+
+ @QuarkusApplication(classes = { PingResource.class, MyBackendPungProxy.class, MyBackendPongProxy.class })
+ static RestService mainPingService = new RestService()
+ .withProperty("stork.pung.service-discovery", "consul")
+ .withProperty("stork.pung.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pung.service-discovery.consul-host", () -> getConsultEndpoint(consul.getConsulEndpoint()))
+ .withProperty("stork.pong-replica.service-discovery", "consul")
+ .withProperty("stork.pong-replica.service-discovery.refresh-period", "1")
+ .withProperty("stork.pong-replica.load-balancer", "round-robin")
+ .withProperty("stork.pong-replica.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pong-replica.service-discovery.consul-host",
+ () -> getConsultEndpoint(consul.getConsulEndpoint()))
+ .withProperty("stork.pong.service-discovery", "consul")
+ .withProperty("stork.pong.service-discovery.refresh-period", "1")
+ .withProperty("stork.pong.load-balancer", "round-robin")
+ .withProperty("stork.pong.service-discovery.consul-port", () -> String.valueOf(consul.getPort()))
+ .withProperty("stork.pong.service-discovery.consul-host", () -> getConsultEndpoint(consul.getConsulEndpoint()));
+
+ @Test
+ public void invokeServiceByName() {
+ String response = makePingCall("pung");
+ assertThat("Service discovery by name fail.", PREFIX + "pung", is(response));
+ }
+
+ @Test
+ public void storkLoadBalancerByRoundRobin() {
+ Map uniqueResp = new HashMap<>();
+ final int requestAmount = 100;
+ final int roundRobinError = (requestAmount / 2) - 1;
+ for (int i = 0; i < requestAmount; i++) {
+ String response = makePingCall("pong");
+ if (uniqueResp.containsKey(response)) {
+ uniqueResp.put(response, uniqueResp.get(response) + 1);
+ } else {
+ uniqueResp.put(response, 1);
+ }
+ }
+
+ Assertions.assertEquals(uniqueResp.size(), 2, "Only 2 services should response");
+ assertThat("Unexpected service names", uniqueResp.keySet(),
+ hasItems(PREFIX + DEFAULT_PONG_RESPONSE, PREFIX + DEFAULT_PONG_REPLICA_RESPONSE));
+ assertThat("Load balancer doesn't behaves as round-robin", uniqueResp.get(PREFIX + DEFAULT_PONG_RESPONSE),
+ is(greaterThanOrEqualTo(roundRobinError)));
+ assertThat("Load balancer doesn't behaves as round-robin", uniqueResp.get(PREFIX + DEFAULT_PONG_REPLICA_RESPONSE),
+ is(greaterThanOrEqualTo(roundRobinError)));
+ }
+
+ private String makePingCall(String subPath) {
+ return mainPingService
+ .given()
+ .get("/ping/" + subPath).then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract().body().asString();
+ }
+
+ private static String getConsultEndpoint(String endpoint) {
+ return endpoint.replaceFirst(":\\d+", "");
+ }
+
+ public static String getAvailablePort() {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return String.valueOf(socket.getLocalPort());
+ } catch (IOException e) {
+ throw new AssertionFailedError();
+ }
+ }
+}
diff --git a/service-discovery/stork/src/test/resources/cluster-role.yaml b/service-discovery/stork/src/test/resources/cluster-role.yaml
new file mode 100644
index 000000000..ad74137aa
--- /dev/null
+++ b/service-discovery/stork/src/test/resources/cluster-role.yaml
@@ -0,0 +1,9 @@
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ namespace: "${NAMESPACE}"
+ name: endpoints-reader
+rules:
+ - apiGroups: [""] # "" indicates the core API group
+ resources: ["endpoints"]
+ verbs: ["get", "watch", "list"]
\ No newline at end of file
diff --git a/service-discovery/stork/src/test/resources/fabric8-rbac.yaml b/service-discovery/stork/src/test/resources/fabric8-rbac.yaml
new file mode 100644
index 000000000..6270952ab
--- /dev/null
+++ b/service-discovery/stork/src/test/resources/fabric8-rbac.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: fabric8-rbac
+subjects:
+ - kind: ServiceAccount
+ # Reference to upper's `metadata.name`
+ name: default
+ namespace: "${NAMESPACE}"
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/service-discovery/stork/src/test/resources/test.properties b/service-discovery/stork/src/test/resources/test.properties
new file mode 100644
index 000000000..71b4dd3ad
--- /dev/null
+++ b/service-discovery/stork/src/test/resources/test.properties
@@ -0,0 +1,2 @@
+ts.pung.openshift.use-internal-service-as-url=true
+ts.pong.openshift.use-internal-service-as-url=true