Skip to content

Commit

Permalink
Test TLS Registry integration with gRPC
Browse files Browse the repository at this point in the history
michalvavrik committed Sep 5, 2024

Verified

This commit was signed with the committer’s verified signature.
michalvavrik Michal Vavřík
1 parent bcc3d14 commit b840c77
Showing 11 changed files with 218 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -3,8 +3,11 @@
import java.util.List;

public final class GrpcReflectionResponse {
private final int serviceCount;
private final List<String> serviceList;
private int serviceCount;
private List<String> serviceList;

public GrpcReflectionResponse() {
}

public GrpcReflectionResponse(int serviceCount, List<String> serviceList) {
this.serviceCount = serviceCount;
@@ -19,4 +22,11 @@ public int getServiceCount() {
return serviceCount;
}

public void setServiceCount(int serviceCount) {
this.serviceCount = serviceCount;
}

public void setServiceList(List<String> serviceList) {
this.serviceList = serviceList;
}
}
18 changes: 18 additions & 0 deletions http/grpc/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -19,3 +19,21 @@ quarkus.grpc.clients.streaming.port=${quarkus.grpc.clients.plain.port}
%ssl.quarkus.http.ssl.certificate.key-files=${grpc.server.key}
%ssl.quarkus.grpc.server.ssl.certificate=${grpc.server.cert}
%ssl.quarkus.grpc.server.ssl.key=${grpc.server.key}

%mtls.quarkus.http.insecure-requests=disabled
%mtls.quarkus.grpc.server.plain-text=false
%mtls.quarkus.grpc.clients.plain.tls-configuration-name=mtls-client
%mtls.quarkus.grpc.clients.reflection-service.tls-configuration-name=mtls-client
%mtls.quarkus.grpc.clients.streaming.tls-configuration-name=mtls-client
%mtls.quarkus.grpc.clients.plain.tls.enabled=true
%mtls.quarkus.grpc.clients.reflection-service.tls.enabled=true
%mtls.quarkus.grpc.clients.streaming.tls.enabled=true
%mtls.quarkus.grpc.clients.plain.plain-text=false
%mtls.quarkus.grpc.clients.reflection-service.plain-text=false
%mtls.quarkus.grpc.clients.streaming.plain-text=false
%mtls.quarkus.grpc.clients.reflection-service.use-quarkus-grpc-client=true
%mtls.quarkus.grpc.clients.streaming.use-quarkus-grpc-client=true
%mtls.quarkus.grpc.clients.plain.port=${quarkus.http.ssl-port}
%mtls.quarkus.tls.mtls-client.key-store.pem.0.cert=${grpc.client.crt}
%mtls.quarkus.tls.mtls-client.key-store.pem.0.key=${grpc.client.key}
%mtls.quarkus.tls.mtls-client.trust-store.pem.certs=${grpc.client.ca-crt}
22 changes: 11 additions & 11 deletions http/grpc/src/test/java/io/quarkus/ts/http/grpc/GRPCIT.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
package io.quarkus.ts.http.grpc;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.ts.grpc.GreeterGrpc;
import io.quarkus.ts.grpc.HelloReply;
import io.quarkus.ts.grpc.HelloRequest;
import io.quarkus.ts.grpc.StreamingGrpc;
import io.vertx.mutiny.ext.web.client.WebClient;

public interface GRPCIT {

RestService app();

CloseableManagedChannel getChannel();

WebClient getWebClient();

@Test
default void grpcClient() {
app().given().get("/http/grpc").then()
.statusCode(HttpStatus.SC_OK)
.body(is("Hello grpc"));
var response = getWebClient().get("/http/grpc").sendAndAwait();
assertEquals(HttpStatus.SC_OK, response.statusCode());
assertTrue(response.bodyAsString().startsWith("Hello grpc"));
}

@Test
default void grpcServer() throws ExecutionException, InterruptedException {
try (var channel = getChannel()) {
HelloRequest request = HelloRequest.newBuilder().setName("server").build();
HelloReply response = GreeterGrpc.newFutureStub(channel).sayHello(request).get();
Assertions.assertEquals("Hello server", response.getMessage());
assertEquals("Hello server", response.getMessage());
}
}

@@ -46,10 +46,10 @@ default void serverStream() {
Iterator<HelloReply> stream = StreamingGrpc.newBlockingStub(channel).serverStream(request);
AtomicInteger counter = new AtomicInteger(0);
stream.forEachRemaining((reply) -> {
Assertions.assertEquals("Hello ServerStream", reply.getMessage());
assertEquals("Hello ServerStream", reply.getMessage());
counter.incrementAndGet();
});
Assertions.assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, counter.get());
assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, counter.get());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.quarkus.ts.http.grpc;

import static io.quarkus.test.security.certificate.CertificateBuilder.INSTANCE_KEY;
import static io.quarkus.test.services.Certificate.Format.PEM;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.condition.OS;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.security.certificate.CertificateBuilder;
import io.quarkus.test.security.certificate.PemClientCertificate;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.Certificate.ClientCertificate;
import io.quarkus.test.services.QuarkusApplication;
import io.vertx.mutiny.ext.web.client.WebClient;

@Tag("QUARKUS-4592")
@QuarkusScenario
public class GrpcMutualTlsSeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT {

private static final String CERT_PREFIX = "grpc-mtls-separate-server";
private static final String CLIENT_CN_NAME = "mtls-client-name";
private static WebClient webClient = null;

@QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, clientCertificates = {
@ClientCertificate(cnAttribute = CLIENT_CN_NAME)
}, format = PEM, configureKeystore = true, configureTruststore = true, tlsConfigName = "mtls-server", configureHttpServer = true))
static final GrpcService app = (GrpcService) new GrpcService()
.withProperty("quarkus.http.ssl.client-auth", "required")
.withProperty("quarkus.profile", "mtls")
.withProperty("grpc.client.crt", GrpcMutualTlsSeparateServerIT::getClientCert)
.withProperty("grpc.client.ca-crt", GrpcMutualTlsSeparateServerIT::getClientCaCert)
.withProperty("grpc.client.key", GrpcMutualTlsSeparateServerIT::getClientKey);

public CloseableManagedChannel getChannel() {
return app.securedGrpcChannel();
}

@Override
public WebClient getWebClient() {
if (webClient == null) {
// HINT: we don't need to close HTTPS client as FW takes care of it
webClient = app.mutinyHttps(CLIENT_CN_NAME);
}
return webClient;
}

private static String getClientCert() {
return addEscapes(getClientCertificate().certPath());
}

private static String getClientCaCert() {
return addEscapes(getClientCertificate().truststorePath());
}

private static String getClientKey() {
return addEscapes(getClientCertificate().keyPath());
}

private static CertificateBuilder getCertificateBuilder() {
return app.getPropertyFromContext(CertificateBuilder.INSTANCE_KEY);
}

private static PemClientCertificate getClientCertificate() {
return (PemClientCertificate) getCertificateBuilder().findCertificateByPrefix(CERT_PREFIX)
.getClientCertificateByCn(CLIENT_CN_NAME);
}

static String addEscapes(String path) {
if (OS.WINDOWS.isCurrentOs()) {
// TODO: move this to the FW
// back-slashes have special meaning in Cygwin etc.
return path.replace("\\", "\\\\");
}
return path;
}
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,68 @@
package io.quarkus.ts.http.grpc;

import static io.quarkus.test.services.Certificate.Format.PEM;
import static io.quarkus.ts.http.grpc.GrpcMutualTlsSeparateServerIT.addEscapes;

import org.junit.jupiter.api.AfterAll;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.security.certificate.Certificate.PemCertificate;
import io.quarkus.test.security.certificate.CertificateBuilder;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;
import io.restassured.specification.RequestSpecification;
import io.vertx.mutiny.ext.web.client.WebClient;

@QuarkusScenario
public class GrpcTlsSeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT {

private static final String CERT_PREFIX = "grpc-tls-separate-server";
private static WebClient webClient = null;

@QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, format = PEM, configureKeystore = true, configureTruststore = true))
static final GrpcService app = (GrpcService) new GrpcService()
.withProperty("quarkus.profile", "ssl")
.withProperty("grpc.client.ca-cert", CertificateBuilder.INSTANCE_KEY, GrpcTlsSeparateServerIT::getClientCaCert)
.withProperty("grpc.server.cert", CertificateBuilder.INSTANCE_KEY, GrpcTlsSeparateServerIT::getServerCert)
.withProperty("grpc.server.key", CertificateBuilder.INSTANCE_KEY, GrpcTlsSeparateServerIT::getServerKey);
.withProperty("grpc.client.ca-cert", GrpcTlsSeparateServerIT::getClientCaCert)
.withProperty("grpc.server.cert", GrpcTlsSeparateServerIT::getServerCert)
.withProperty("grpc.server.key", GrpcTlsSeparateServerIT::getServerKey);

public CloseableManagedChannel getChannel() {
return app.securedGrpcChannel();
}

@Override
public RestService app() {
return app;
public WebClient getWebClient() {
if (webClient == null) {
webClient = app.mutiny();
}
return webClient;
}

@Override
public RequestSpecification given() {
return app().relaxedHttps().given();
@AfterAll
static void afterAll() {
if (webClient != null) {
webClient.close();
}
}

private static String getClientCaCert() {
return addEscapes(getPemCertificate().truststorePath());
}

private static String getClientCaCert(CertificateBuilder certificateBuilder) {
return getPemCertificate(certificateBuilder).truststorePath();
private static String getServerCert() {
return addEscapes(getPemCertificate().certPath());
}

private static String getServerCert(CertificateBuilder certificateBuilder) {
return getPemCertificate(certificateBuilder).certPath();
private static String getServerKey() {
return addEscapes(getPemCertificate().keyPath());
}

private static String getServerKey(CertificateBuilder certificateBuilder) {
return getPemCertificate(certificateBuilder).keyPath();
private static PemCertificate getPemCertificate() {
return (PemCertificate) getCertificateBuilder().findCertificateByPrefix(CERT_PREFIX);
}

private static PemCertificate getPemCertificate(CertificateBuilder certificateBuilder) {
return (PemCertificate) certificateBuilder.findCertificateByPrefix(CERT_PREFIX);
private static CertificateBuilder getCertificateBuilder() {
return app.getPropertyFromContext(CertificateBuilder.INSTANCE_KEY);
}
}
Original file line number Diff line number Diff line change
@@ -4,22 +4,22 @@
import org.junit.jupiter.api.Tag;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy;
import io.quarkus.test.scenarios.OpenShiftScenario;
import io.vertx.mutiny.ext.web.client.WebClient;

@Tag("use-quarkus-openshift-extension")
@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtension)
@Disabled("https://github.com/quarkus-qe/quarkus-test-framework/issues/1052+1053")
public class OpenShiftExtensionGRPCIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT {

@Override
public RestService app() {
public CloseableManagedChannel getChannel() {
return null;
}

@Override
public CloseableManagedChannel getChannel() {
public WebClient getWebClient() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -3,20 +3,20 @@
import org.junit.jupiter.api.Disabled;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.OpenShiftScenario;
import io.vertx.mutiny.ext.web.client.WebClient;

@OpenShiftScenario
@Disabled("https://github.com/quarkus-qe/quarkus-test-framework/issues/1052+1053")
public class OpenShiftGRPCIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT {

@Override
public RestService app() {
public CloseableManagedChannel getChannel() {
return null;
}

@Override
public CloseableManagedChannel getChannel() {
public WebClient getWebClient() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
package io.quarkus.ts.http.grpc;

import static org.apache.http.HttpStatus.SC_OK;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.google.protobuf.Descriptors;
import com.google.protobuf.InvalidProtocolBufferException;

import io.grpc.reflection.v1.FileDescriptorResponse;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.ts.grpc.GreeterGrpc;
import io.quarkus.ts.grpc.HelloWorldProto;
import io.quarkus.ts.grpc.StreamingGrpc;
import io.vertx.mutiny.ext.web.client.WebClient;

public interface ReflectionHttpIT {

RestService app();
WebClient getWebClient();

@Test
default void testReflectionServices() {
GrpcReflectionResponse response = app().given().when().get("/http/reflection/service/info")
.then().statusCode(SC_OK).extract().response()
.jsonPath().getObject(".", GrpcReflectionResponse.class);
var httpResponse = getWebClient().get("/http/reflection/service/info").sendAndAwait();
assertEquals(SC_OK, httpResponse.statusCode());
GrpcReflectionResponse response = httpResponse.bodyAsJson(GrpcReflectionResponse.class);

assertEquals(3, response.getServiceCount());

@@ -43,8 +41,9 @@ default void testReflectionServices() {

@Test
default void testReflectionMethods() throws InvalidProtocolBufferException {
byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting")
.then().statusCode(SC_OK).extract().body().asByteArray();
var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait();
assertEquals(SC_OK, httpResponse.statusCode());
byte[] responseByteArray = httpResponse.body().getBytes();

String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString();

@@ -72,8 +71,9 @@ default void testReflectionMethods() throws InvalidProtocolBufferException {
@Test
@DisplayName("GRPC reflection test - check service messages types")
default void testReflectionMessages() throws InvalidProtocolBufferException {
byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting")
.then().statusCode(SC_OK).extract().body().asByteArray();
var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait();
assertEquals(SC_OK, httpResponse.statusCode());
byte[] responseByteArray = httpResponse.body().getBytes();
String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString();

var messageTypes = HelloWorldProto.getDescriptor().getMessageTypes();
@@ -87,8 +87,9 @@ default void testReflectionMessages() throws InvalidProtocolBufferException {
@Test
@DisplayName("GRPC reflection test - check method SayHello of Greeter service exists and then call it")
default void testReflectionCallMethod() throws InvalidProtocolBufferException {
byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting")
.then().statusCode(SC_OK).extract().body().asByteArray();
var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait();
assertEquals(SC_OK, httpResponse.statusCode());
byte[] responseByteArray = httpResponse.body().getBytes();

String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString();

@@ -111,6 +112,8 @@ default void testReflectionCallMethod() throws InvalidProtocolBufferException {
assertTrue(fileDescriptor.contains("SayHello"));

// Call sayHello method and compare context
app().given().when().get("/http/tester").then().statusCode(SC_OK).body(Matchers.is("Hello tester"));
httpResponse = getWebClient().get("/http/tester").sendAndAwait();
assertEquals(SC_OK, httpResponse.statusCode());
assertEquals("Hello tester", httpResponse.bodyAsString());
}
}
19 changes: 16 additions & 3 deletions http/grpc/src/test/java/io/quarkus/ts/http/grpc/SameServerIT.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package io.quarkus.ts.http.grpc;

import org.junit.jupiter.api.AfterAll;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.QuarkusApplication;
import io.vertx.mutiny.ext.web.client.WebClient;

@QuarkusScenario
public class SameServerIT implements GRPCIT, ReflectionHttpIT, StreamingHttpIT {

private static WebClient webClient = null;

@QuarkusApplication(grpc = true)
static final GrpcService app = new GrpcService();

@@ -18,8 +22,17 @@ public CloseableManagedChannel getChannel() {
}

@Override
public RestService app() {
return app;
public WebClient getWebClient() {
if (webClient == null) {
webClient = app.mutiny();
}
return webClient;
}

@AfterAll
static void afterAll() {
if (webClient != null) {
webClient.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package io.quarkus.ts.http.grpc;

import org.junit.jupiter.api.AfterAll;

import io.quarkus.test.bootstrap.CloseableManagedChannel;
import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.QuarkusApplication;
import io.vertx.mutiny.ext.web.client.WebClient;

@QuarkusScenario
public class SeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT {

private static WebClient webClient = null;

@QuarkusApplication(grpc = true)
static final GrpcService app = (GrpcService) new GrpcService()
.withProperty("quarkus.grpc.server.use-separate-server", "true")
@@ -20,8 +24,17 @@ public CloseableManagedChannel getChannel() {
}

@Override
public RestService app() {
return app;
public WebClient getWebClient() {
if (webClient == null) {
webClient = app.mutiny();
}
return webClient;
}

@AfterAll
static void afterAll() {
if (webClient != null) {
webClient.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -5,48 +5,40 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.RestService;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.vertx.core.http.HttpHeaders;
import io.vertx.mutiny.ext.web.client.WebClient;

public interface StreamingHttpIT {

RestService app();

default RequestSpecification given() {
return app().given();
}
WebClient getWebClient();

@Test
default void serverStreaming() {
Response response = given().when().get("/http/streaming/server/ServerStreaming");
var response = getWebClient().get("/http/streaming/server/ServerStreaming").sendAndAwait();
Assertions.assertEquals(200, response.statusCode());
List<String> responses = response.jsonPath().getList(".");
List<?> responses = response.bodyAsJsonArray().getList();
Assertions.assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, responses.size());
responses.forEach(message -> Assertions.assertEquals("Hello ServerStreaming", message));
}

@Test
default void clientStreaming() {
List<String> names = List.of("Alice", "Bob", "Charlie");
Response response = given().when()
.contentType(ContentType.JSON)
.body(names)
.post("/http/streaming/client");
var response = getWebClient().post("/http/streaming/client")
.putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json")
.sendJsonAndAwait(names);
Assertions.assertEquals(200, response.statusCode());
Assertions.assertEquals("Total names submitted: " + names.size(), response.body().asString());
Assertions.assertEquals("Total names submitted: " + names.size(), response.bodyAsString());
}

@Test
default void bidirectional() {
List<String> names = List.of("Alice", "Bob", "Charlie");
Response response = given().when()
.contentType(ContentType.JSON)
.body(names)
.post("/http/streaming/bi");
var response = getWebClient().post("/http/streaming/bi")
.putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json")
.sendJsonAndAwait(names);
Assertions.assertEquals(200, response.statusCode());
List<String> messages = response.jsonPath().getList(".");
var messages = response.bodyAsJsonArray().getList();
Assertions.assertEquals(names.size() + 1, messages.size());
Assertions.assertEquals("Hello: Alice;Bob;Charlie;", messages.get(names.size()));
}

0 comments on commit b840c77

Please sign in to comment.