diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 757796832ba9fb..46fac6f133912d 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -2170,6 +2170,11 @@
quarkus-grpc-stubs
${project.version}
+
+ io.quarkus
+ quarkus-grpc-reflection
+ ${project.version}
+
io.quarkus
quarkus-grpc-deployment
diff --git a/docs/src/main/asciidoc/grpc-cli.adoc b/docs/src/main/asciidoc/grpc-cli.adoc
new file mode 100644
index 00000000000000..9ccafc49a487a0
--- /dev/null
+++ b/docs/src/main/asciidoc/grpc-cli.adoc
@@ -0,0 +1,130 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
+////
+= Using gRPC CLI
+include::_attributes.adoc[]
+:categories: serialization
+:summary: This page explains how to use gRPC CLI.
+:topics: grpc,cli
+:extensions: io.quarkus:quarkus-grpc-cli
+
+This page explains how to use gRPC CLI -- a grpcurl-like tool.
+
+See basic Quarkus CLI usage first:
+
+* xref:cli-tooling.adoc[CLI tooling]
+
+== Examples
+
+Display gRPC CLI help:
+
+[source,shell]
+----
+grpc
+Usage: grpc [-h] [COMMAND]
+ -h, --help Display this help message.
+Commands:
+ list grpc list
+ describe grpc describe
+ invoke grpc invoke
+----
+
+List all available gRPC services:
+
+[source,shell]
+----
+grpc list localhost:8080
+
+helloworld.Greeter
+grpc.health.v1.Health
+----
+
+Describe a service:
+
+[source,shell]
+----
+grpc describe localhost:8080 helloworld.Greeter
+
+{
+ "name": "helloworld.proto",
+ "package": "helloworld",
+ "dependency": ["google/protobuf/empty.proto"],
+ "messageType": [{
+ "name": "HelloRequest",
+ "field": [{
+ "name": "name",
+ "number": 1,
+ "label": "LABEL_OPTIONAL",
+ "type": "TYPE_STRING"
+ }]
+ }, {
+ "name": "HelloReply",
+ "field": [{
+ "name": "message",
+ "number": 1,
+ "label": "LABEL_OPTIONAL",
+ "type": "TYPE_STRING"
+ }]
+ }],
+ "service": [{
+ "name": "Greeter",
+ "method": [{
+ "name": "SayHello",
+ "inputType": ".helloworld.HelloRequest",
+ "outputType": ".helloworld.HelloReply",
+ "options": {
+ }
+ }, {
+ "name": "SayJo",
+ "inputType": ".google.protobuf.Empty",
+ "outputType": ".helloworld.HelloReply",
+ "options": {
+ }
+ }, {
+ "name": "ThreadName",
+ "inputType": ".helloworld.HelloRequest",
+ "outputType": ".helloworld.HelloReply",
+ "options": {
+ }
+ }]
+ }],
+ "options": {
+ "javaPackage": "examples",
+ "javaOuterClassname": "HelloWorldProto",
+ "javaMultipleFiles": true,
+ "objcClassPrefix": "HLW"
+ },
+ "syntax": "proto3"
+}
+{
+ "name": "google/protobuf/empty.proto",
+ "package": "google.protobuf",
+ "messageType": [{
+ "name": "Empty"
+ }],
+ "options": {
+ "javaPackage": "com.google.protobuf",
+ "javaOuterClassname": "EmptyProto",
+ "javaMultipleFiles": true,
+ "goPackage": "google.golang.org/protobuf/types/known/emptypb",
+ "ccEnableArenas": true,
+ "objcClassPrefix": "GPB",
+ "csharpNamespace": "Google.Protobuf.WellKnownTypes"
+ },
+ "syntax": "proto3"
+}
+----
+
+Invoke a service method:
+
+[source,shell]
+----
+grpc invoke localhost:8080 helloworld.Greeter/SayHello -d '{"name" : "gRPC"}'
+
+Invoking method: helloworld.Greeter/SayHello
+{
+ "message": "Hello gRPC"
+}
+----
diff --git a/docs/src/main/asciidoc/grpc.adoc b/docs/src/main/asciidoc/grpc.adoc
index d0a7abe6bb1514..bc16b0bef4cae5 100644
--- a/docs/src/main/asciidoc/grpc.adoc
+++ b/docs/src/main/asciidoc/grpc.adoc
@@ -36,3 +36,4 @@ Quarkus gRPC is based on https://vertx.io/docs/vertx-grpc/java/[Vert.x gRPC].
* xref:grpc-xds.adoc[Enabling xDS gRPC support]
* xref:grpc-generation-reference.adoc[gRPC code generation reference guide]
* xref:grpc-reference.adoc[gRPC reference guide]
+* xref:grpc-cli.adoc[gRPC CLI support]
diff --git a/extensions/grpc/cli/pom.xml b/extensions/grpc/cli/pom.xml
new file mode 100644
index 00000000000000..9b615e9802766d
--- /dev/null
+++ b/extensions/grpc/cli/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+ quarkus-grpc-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ quarkus-grpc-cli
+ Quarkus - gRPC - CLI
+ gRPC CLI
+
+
+ io.quarkus.grpc.cli.GrpcCommand
+
+
+
+
+ io.vertx
+ vertx-grpc-client
+
+
+ com.google.protobuf
+ protobuf-java-util
+
+
+ io.quarkus
+ quarkus-grpc-reflection
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-picocli
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.quarkus
+ quarkus-grpc
+ test
+
+
+
+ io.quarkus
+ quarkus-grpc-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ -parameters
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+ jar-with-dependencies
+
+ ${project.artifactId}-${project.version}
+ false
+
+
+ true
+ ${main.class}
+
+
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java
new file mode 100644
index 00000000000000..7a8a9698429992
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/DescribeCommand.java
@@ -0,0 +1,44 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.util.JsonFormat;
+
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.smallrye.mutiny.Multi;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "describe", sortOptions = false, header = "gRPC describe")
+public class DescribeCommand extends GcurlBaseCommand {
+
+ public String getAction() {
+ return "describe";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setFileContainingSymbol(unmatched.get(1))
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.toUni().map(r -> {
+ List list = r.getFileDescriptorResponse().getFileDescriptorProtoList();
+ for (ByteString bs : list) {
+ try {
+ DescriptorProtos.FileDescriptorProto fdp = DescriptorProtos.FileDescriptorProto.parseFrom(bs);
+ log(JsonFormat.printer().print(fdp));
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return null;
+ }).await().indefinitely();
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java
new file mode 100644
index 00000000000000..5ac5ebfa34276b
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GcurlBaseCommand.java
@@ -0,0 +1,132 @@
+package io.quarkus.grpc.cli;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ProtocolStringList;
+
+import io.grpc.Channel;
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.grpc.client.GrpcClient;
+import io.vertx.grpc.client.GrpcClientChannel;
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Spec;
+
+public abstract class GcurlBaseCommand implements Callable {
+
+ @Spec
+ CommandSpec spec;
+
+ @CommandLine.Unmatched
+ List unmatched;
+
+ Vertx vertx = Vertx.vertx();
+
+ /**
+ * The grpc subcommand (e.g. list, describe, invoke)
+ *
+ * @return the subcommand
+ */
+ protected abstract String getAction();
+
+ protected abstract void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub);
+
+ protected void log(String msg) {
+ System.out.println(msg);
+ }
+
+ protected void err(String msg) {
+ System.err.println(msg);
+ }
+
+ protected static List getFileDescriptorsFromProtos(List protos) {
+ try {
+ Map all = protos
+ .stream()
+ .map(bs -> {
+ try {
+ return DescriptorProtos.FileDescriptorProto.parseFrom(bs);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toMap(DescriptorProtos.FileDescriptorProto::getName, Function.identity(), (a, b) -> a));
+ List fds = new ArrayList<>();
+ Map resolved = new HashMap<>();
+ for (DescriptorProtos.FileDescriptorProto fdp : all.values()) {
+ fds.add(toFileDescriptor(fdp, all, resolved));
+ }
+ return fds;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Descriptors.FileDescriptor toFileDescriptor(DescriptorProtos.FileDescriptorProto fdp,
+ Map all, Map resolved) {
+ int n = fdp.getDependencyCount();
+ ProtocolStringList list = fdp.getDependencyList();
+ Descriptors.FileDescriptor[] fds = new Descriptors.FileDescriptor[n];
+ for (int i = 0; i < n; i++) {
+ String dep = list.get(i);
+ // remember resolved FDs, recursively resolve deps
+ fds[i] = resolved.computeIfAbsent(dep, key -> {
+ DescriptorProtos.FileDescriptorProto proto = all.get(key);
+ return toFileDescriptor(proto, all, resolved);
+ });
+ }
+ try {
+ return Descriptors.FileDescriptor.buildFrom(fdp, fds);
+ } catch (Descriptors.DescriptorValidationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Integer call() {
+ if (unmatched == null || unmatched.isEmpty()) {
+ log("Missing host:port");
+ return CommandLine.ExitCode.USAGE;
+ }
+
+ return execute(channel -> {
+ try {
+ MutinyServerReflectionGrpc.MutinyServerReflectionStub stub = MutinyServerReflectionGrpc.newMutinyStub(channel);
+ execute(stub);
+ return CommandLine.ExitCode.OK;
+ } catch (Exception e) {
+ err("Failed to execute grpc " + getAction() + ", due to: " + e.getMessage());
+ return CommandLine.ExitCode.SOFTWARE;
+ }
+ });
+ }
+
+ protected X execute(Function fn) {
+ HttpClientOptions options = new HttpClientOptions(); // TODO
+ options.setHttp2ClearTextUpgrade(false);
+
+ GrpcClient client = GrpcClient.client(vertx, options);
+ String[] split = unmatched.get(0).split(":");
+ String host = split[0];
+ int port = Integer.parseInt(split[1]);
+ Channel channel = new GrpcClientChannel(client, SocketAddress.inetSocketAddress(port, host));
+ try {
+ return fn.apply(channel);
+ } finally {
+ client.close().toCompletionStage().toCompletableFuture().join();
+ }
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java
new file mode 100644
index 00000000000000..480ddd2e581835
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java
@@ -0,0 +1,32 @@
+package io.quarkus.grpc.cli;
+
+import java.util.concurrent.Callable;
+
+import io.quarkus.picocli.runtime.annotations.TopCommand;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Spec;
+
+@TopCommand
+@Command(name = "grpc", sortOptions = false, header = "grpc", subcommands = {
+ ListCommand.class, DescribeCommand.class, InvokeCommand.class })
+public class GrpcCommand implements Callable {
+
+ @Spec
+ protected CommandSpec spec;
+
+ @CommandLine.Option(names = { "-h", "--help" }, usageHelp = true, description = "Display this help message.")
+ public boolean help;
+
+ @Override
+ public Integer call() {
+ CommandLine schemaCommand = spec.subcommands().get("list");
+ return schemaCommand.execute();
+ }
+
+ public static void main(String[] args) {
+ int exitCode = new CommandLine(new GrpcCommand()).execute(args);
+ System.exit(exitCode);
+ }
+}
\ No newline at end of file
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java
new file mode 100644
index 00000000000000..f6c15a7d947bd1
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java
@@ -0,0 +1,120 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+
+import io.grpc.CallOptions;
+import io.grpc.MethodDescriptor;
+import io.grpc.protobuf.ProtoUtils;
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.stub.ClientCalls;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.infrastructure.Infrastructure;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "invoke", sortOptions = false, header = "gRPC invoke")
+public class InvokeCommand extends GcurlBaseCommand {
+
+ @CommandLine.Option(names = { "-d" }, description = "Request input")
+ Optional content;
+
+ public String getAction() {
+ return "invoke";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ String toInvoke = unmatched.get(1);
+ String[] split = toInvoke.split("/");
+ String serviceName = split[0];
+ String methodName = split[1];
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setFileContainingSymbol(serviceName)
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.emitOn(Infrastructure.getDefaultWorkerPool()).toUni().map(r -> {
+ ServerReflectionResponse.MessageResponseCase responseCase = r.getMessageResponseCase();
+ if (responseCase == ServerReflectionResponse.MessageResponseCase.FILE_DESCRIPTOR_RESPONSE) {
+ List byteStrings = r.getFileDescriptorResponse().getFileDescriptorProtoList();
+ for (Descriptors.FileDescriptor fd : getFileDescriptorsFromProtos(byteStrings)) {
+ fd.getServices().forEach(
+ sd -> {
+ String fullName = sd.getFullName();
+ if (fullName.equals(serviceName)) {
+ Descriptors.MethodDescriptor md = sd.findMethodByName(methodName);
+ if (md != null) {
+ invokeMethod(md);
+ } else {
+ log("Method not found: " + methodName);
+ }
+ }
+ });
+ }
+ } else {
+ err("Unexpected response from server reflection: " + responseCase);
+ }
+ return null;
+ }).await().indefinitely();
+ }
+
+ private void invokeMethod(Descriptors.MethodDescriptor md) {
+ String fullMethodName = md.getService().getFullName() + "/" + md.getName();
+ log("Invoking method: " + fullMethodName);
+ Descriptors.Descriptor inputType = md.getInputType();
+ DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(inputType);
+ try {
+ content.ifPresent(request -> {
+ try {
+ JsonFormat.parser().merge(request, messageBuilder);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ DynamicMessage msg = messageBuilder.build();
+ MethodDescriptor.MethodType methodType = MethodDescriptor.MethodType.UNARY;
+ if (md.isClientStreaming()) {
+ methodType = MethodDescriptor.MethodType.CLIENT_STREAMING;
+ }
+ if (md.isServerStreaming()) {
+ methodType = MethodDescriptor.MethodType.SERVER_STREAMING;
+ }
+ if (md.isClientStreaming() && md.isServerStreaming()) {
+ methodType = MethodDescriptor.MethodType.BIDI_STREAMING;
+ }
+ MethodDescriptor methodDescriptor = io.grpc.MethodDescriptor
+ . newBuilder()
+ .setType(methodType)
+ .setFullMethodName(fullMethodName)
+ .setRequestMarshaller(ProtoUtils.marshaller(DynamicMessage.getDefaultInstance(inputType)))
+ .setResponseMarshaller(
+ ProtoUtils.marshaller(DynamicMessage.getDefaultInstance(md.getOutputType())))
+ .build();
+
+ execute(channel -> {
+ DynamicMessage response = ClientCalls.blockingUnaryCall(
+ channel,
+ methodDescriptor,
+ CallOptions.DEFAULT,
+ msg);
+
+ try {
+ log(JsonFormat.printer().print(response));
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ });
+ } catch (Exception e) {
+ err("Error creating dynamic message: " + e.getMessage());
+ }
+ }
+}
diff --git a/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java
new file mode 100644
index 00000000000000..89790ef8f9a160
--- /dev/null
+++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/ListCommand.java
@@ -0,0 +1,34 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+
+import io.grpc.reflection.v1.MutinyServerReflectionGrpc;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.reflection.v1.ServiceResponse;
+import io.smallrye.mutiny.Multi;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "list", sortOptions = false, header = "gRPC list")
+public class ListCommand extends GcurlBaseCommand {
+
+ public String getAction() {
+ return "list";
+ }
+
+ @Override
+ protected void execute(MutinyServerReflectionGrpc.MutinyServerReflectionStub stub) {
+ ServerReflectionRequest request = ServerReflectionRequest
+ .newBuilder()
+ .setListServices("dummy")
+ .build();
+ Multi response = stub.serverReflectionInfo(Multi.createFrom().item(request));
+ response.toUni().map(r -> {
+ List serviceList = r.getListServicesResponse().getServiceList();
+ serviceList.forEach(sr -> {
+ log(sr.getName());
+ });
+ return null;
+ }).await().indefinitely();
+ }
+}
diff --git a/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/GrpcCliTest.java b/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/GrpcCliTest.java
new file mode 100644
index 00000000000000..48f2cd78b08dc3
--- /dev/null
+++ b/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/GrpcCliTest.java
@@ -0,0 +1,57 @@
+package io.quarkus.grpc.cli;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class GrpcCliTest {
+
+ @Test
+ public void testCommand() throws Exception {
+ StringBuffer buffer = new StringBuffer();
+
+ ListCommand listCommand = new ListCommand() {
+ @Override
+ protected void log(String msg) {
+ buffer.append(msg).append("\n");
+ }
+ };
+ listCommand.unmatched = List.of("localhost:8081");
+ Integer exitCode = listCommand.call();
+ Assertions.assertEquals(0, exitCode);
+ Assertions.assertTrue(buffer.toString().contains("helloworld.Greeter"));
+ buffer.setLength(0);
+
+ DescribeCommand describeCommand = new DescribeCommand() {
+ @Override
+ protected void log(String msg) {
+ buffer.append(msg).append("\n");
+ }
+ };
+ describeCommand.unmatched = List.of("localhost:8081", "helloworld.Greeter");
+ exitCode = describeCommand.call();
+ Assertions.assertEquals(0, exitCode);
+ String string = buffer.toString();
+ Assertions.assertTrue(string.contains("HelloRequest"));
+ Assertions.assertTrue(string.contains("HelloReply"));
+ buffer.setLength(0);
+
+ InvokeCommand invokeCommand = new InvokeCommand() {
+ @Override
+ protected void log(String msg) {
+ buffer.append(msg).append("\n");
+ }
+ };
+ invokeCommand.unmatched = List.of("localhost:8081", "helloworld.Greeter/SayHello");
+ invokeCommand.content = Optional.of("{name : \"Quarkus\"}");
+ exitCode = invokeCommand.call();
+ Assertions.assertEquals(0, exitCode);
+ string = buffer.toString();
+ Assertions.assertTrue(string.contains("Hello Quarkus"));
+ }
+}
diff --git a/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/support/HelloWorldService.java b/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/support/HelloWorldService.java
new file mode 100644
index 00000000000000..7266b6ddd3e142
--- /dev/null
+++ b/extensions/grpc/cli/src/test/java/io/quarkus/grpc/cli/support/HelloWorldService.java
@@ -0,0 +1,42 @@
+package io.quarkus.grpc.cli.support;
+
+import examples.GreeterGrpc;
+import examples.HelloReply;
+import examples.HelloRequest;
+import io.grpc.stub.StreamObserver;
+import io.quarkus.grpc.GrpcService;
+
+@GrpcService
+public class HelloWorldService extends GreeterGrpc.GreeterImplBase {
+
+ private HelloReply getReply(HelloRequest request) {
+ String name = request.getName();
+ return HelloReply.newBuilder().setMessage("Hello " + name).build();
+ }
+
+ @Override
+ public void sayHello(HelloRequest request, StreamObserver responseObserver) {
+ responseObserver.onNext(getReply(request));
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public StreamObserver multiHello(StreamObserver responseObserver) {
+ return new StreamObserver<>() {
+ @Override
+ public void onNext(HelloRequest helloRequest) {
+ responseObserver.onNext(getReply(helloRequest));
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ responseObserver.onError(throwable);
+ }
+
+ @Override
+ public void onCompleted() {
+ responseObserver.onCompleted();
+ }
+ };
+ }
+}
diff --git a/extensions/grpc/cli/src/test/proto/helloworld.proto b/extensions/grpc/cli/src/test/proto/helloworld.proto
new file mode 100644
index 00000000000000..f20a66ff856d38
--- /dev/null
+++ b/extensions/grpc/cli/src/test/proto/helloworld.proto
@@ -0,0 +1,54 @@
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "examples";
+option java_outer_classname = "HelloWorldProto";
+option objc_class_prefix = "HLW";
+
+package helloworld;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+ rpc MultiHello (stream HelloRequest) returns (stream HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+ string message = 1;
+}
diff --git a/extensions/grpc/cli/src/test/resources/application.properties b/extensions/grpc/cli/src/test/resources/application.properties
new file mode 100644
index 00000000000000..33656d6c79510c
--- /dev/null
+++ b/extensions/grpc/cli/src/test/resources/application.properties
@@ -0,0 +1,3 @@
+quarkus.grpc.server.use-separate-server=false
+quarkus.grpc.server.enable-reflection-service=true
+
diff --git a/extensions/grpc/pom.xml b/extensions/grpc/pom.xml
index da3eb3e1eed285..32c150f5b3c140 100644
--- a/extensions/grpc/pom.xml
+++ b/extensions/grpc/pom.xml
@@ -18,9 +18,11 @@
codegen
api
stubs
+ reflection
deployment
runtime
xds
inprocess
+ cli
\ No newline at end of file
diff --git a/extensions/grpc/reflection/pom.xml b/extensions/grpc/reflection/pom.xml
new file mode 100644
index 00000000000000..c8e6d9fac8e619
--- /dev/null
+++ b/extensions/grpc/reflection/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ quarkus-grpc-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+
+ quarkus-grpc-reflection
+ Quarkus - gRPC - Reflection
+ Reflection gRPC services and utils
+
+
+ io.quarkus
+ quarkus-grpc-api
+
+
+ io.quarkus
+ quarkus-grpc-stubs
+
+
+ *
+ *
+
+
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+ com.google.guava
+ guava
+
+
+ com.google.errorprone
+ error_prone_annotations
+
+
+
+
+ io.grpc
+ grpc-stub
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+
+ io.quarkus
+ quarkus-mutiny
+
+
+
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
index 5c1dece314e527..1786106fb1be46 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/GrpcServerIndex.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
index 0f57ad617c4f01..1c668087de500d 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
similarity index 99%
rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java
rename to extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
index 1caafa1a4425bf..a590fdfe2a95a1 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1alpha.java
+++ b/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1alpha.java
@@ -1,4 +1,4 @@
-package io.quarkus.grpc.runtime.reflection;
+package io.quarkus.grpc.reflection.service;
import static com.google.protobuf.Descriptors.FileDescriptor;
diff --git a/extensions/grpc/runtime/pom.xml b/extensions/grpc/runtime/pom.xml
index 73393d614ff219..35442249814cc2 100644
--- a/extensions/grpc/runtime/pom.xml
+++ b/extensions/grpc/runtime/pom.xml
@@ -25,6 +25,10 @@
io.quarkus
quarkus-grpc-common
+
+ io.quarkus
+ quarkus-grpc-reflection
+
io.quarkus
quarkus-vertx-http
@@ -121,6 +125,12 @@
+
+
+ src/main/resources
+ true
+
+
io.quarkus
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
index 322cf5eb18db7c..dfcbd00be93d55 100644
--- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java
@@ -42,6 +42,8 @@
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.Subclass;
import io.quarkus.grpc.auth.GrpcSecurityInterceptor;
+import io.quarkus.grpc.reflection.service.ReflectionServiceV1;
+import io.quarkus.grpc.reflection.service.ReflectionServiceV1alpha;
import io.quarkus.grpc.runtime.config.GrpcConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerNettyConfig;
@@ -49,8 +51,6 @@
import io.quarkus.grpc.runtime.devmode.GrpcHotReplacementInterceptor;
import io.quarkus.grpc.runtime.devmode.GrpcServerReloader;
import io.quarkus.grpc.runtime.health.GrpcHealthStorage;
-import io.quarkus.grpc.runtime.reflection.ReflectionServiceV1;
-import io.quarkus.grpc.runtime.reflection.ReflectionServiceV1alpha;
import io.quarkus.grpc.runtime.supports.CompressionInterceptor;
import io.quarkus.grpc.runtime.supports.blocking.BlockingServerInterceptor;
import io.quarkus.grpc.spi.GrpcBuilderProvider;
diff --git a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
index 2a2f8152e8fe75..ad0bc59f0e4ce7 100644
--- a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
+++ b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -16,3 +16,5 @@ metadata:
artifact: "io.quarkus:quarkus-project-core-extension-codestarts"
config:
- "quarkus.grpc."
+ cli-plugins:
+ - "${project.groupId}:quarkus-grpc-cli:${project.version}"
\ No newline at end of file
diff --git a/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java b/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
index d41dcca08d0401..ea5d5d38ddf6e3 100644
--- a/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
+++ b/integration-tests/grpc-vertx/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java
@@ -1,5 +1,7 @@
package io.quarkus.grpc.examples.hello;
+import com.google.protobuf.Empty;
+
import examples.HelloReply;
import examples.HelloRequest;
import examples.MutinyGreeterGrpc;
@@ -16,6 +18,12 @@ public Uni sayHello(HelloRequest request) {
.map(res -> HelloReply.newBuilder().setMessage(res).build());
}
+ @Override
+ public Uni sayJo(Empty request) {
+ return Uni.createFrom().item("Jo!")
+ .map(res -> HelloReply.newBuilder().setMessage(res).build());
+ }
+
@Override
public Uni threadName(HelloRequest request) {
return Uni.createFrom().item(Thread.currentThread().getName())
diff --git a/integration-tests/grpc-vertx/src/main/proto/helloworld.proto b/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
index c798d111640b48..6ae0904fb041d8 100644
--- a/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
+++ b/integration-tests/grpc-vertx/src/main/proto/helloworld.proto
@@ -36,10 +36,14 @@ option objc_class_prefix = "HLW";
package helloworld;
+// Import the empty message definition
+import "google/protobuf/empty.proto";
+
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
+ rpc SayJo (google.protobuf.Empty) returns (HelloReply) {}
rpc ThreadName (HelloRequest) returns (HelloReply) {}
}