diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 28bcbfe8b791e..c4608efc8e3fd 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2170,6 +2170,16 @@ quarkus-grpc-stubs ${project.version} + + io.quarkus + quarkus-grpc-reflection + ${project.version} + + + io.quarkus + quarkus-grpc-cli + ${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 0000000000000..d745b12358e22 --- /dev/null +++ b/docs/src/main/asciidoc/grpc-cli.adoc @@ -0,0 +1,129 @@ +//// +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] +---- +quarkus 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] +---- +quarkus grpc list localhost:8080 + +helloworld.Greeter +grpc.health.v1.Health +---- + +Describe a service: + +[source,shell] +---- +quarkus 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] +---- +quarkus grpc invoke localhost:8080 helloworld.Greeter/SayHello -d '{"name" : "gRPC"}' + +{ + "message": "Hello gRPC" +} +---- diff --git a/docs/src/main/asciidoc/grpc.adoc b/docs/src/main/asciidoc/grpc.adoc index d0a7abe6bb151..bc16b0bef4cae 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 0000000000000..1e4c5892675f4 --- /dev/null +++ b/extensions/grpc/cli/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + quarkus-grpc-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-grpc-cli + Quarkus - gRPC - CLI + gRPC CLI + + + io.quarkus.grpc.cli.GrpcCommand + + + + + info.picocli + picocli + + + io.vertx + vertx-grpc-client + + + com.google.protobuf + protobuf-java-util + + + io.quarkus + quarkus-grpc-reflection + + + + + + 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 0000000000000..7a8a969842999 --- /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 0000000000000..5ac5ebfa34276 --- /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 0000000000000..4dc3e0cbff1f6 --- /dev/null +++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/GrpcCommand.java @@ -0,0 +1,30 @@ +package io.quarkus.grpc.cli; + +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +@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 0000000000000..768db40a37be5 --- /dev/null +++ b/extensions/grpc/cli/src/main/java/io/quarkus/grpc/cli/InvokeCommand.java @@ -0,0 +1,119 @@ +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(); + 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 0000000000000..89790ef8f9a16 --- /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/pom.xml b/extensions/grpc/pom.xml index da3eb3e1eed28..32c150f5b3c14 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 0000000000000..c8e6d9fac8e61 --- /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 5c1dece314e52..1786106fb1be4 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 0f57ad617c4f0..1c668087de500 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 1caafa1a4425b..a590fdfe2a95a 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 73393d614ff21..35442249814cc 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 322cf5eb18db7..dfcbd00be93d5 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 2a2f8152e8fe7..ad0bc59f0e4ce 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-cli/pom.xml b/integration-tests/grpc-cli/pom.xml new file mode 100644 index 0000000000000..8133bd9021b21 --- /dev/null +++ b/integration-tests/grpc-cli/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-cli + Quarkus - Integration Tests - gRPC - CLI + + + + io.quarkus + quarkus-grpc + + + io.quarkus + quarkus-grpc-cli + test + + + io.quarkus + quarkus-junit5 + test + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-grpc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + generate-code + build + + + + + + + + diff --git a/integration-tests/grpc-cli/src/main/java/io/quarkus/grpc/examples/cli/HelloWorldService.java b/integration-tests/grpc-cli/src/main/java/io/quarkus/grpc/examples/cli/HelloWorldService.java new file mode 100644 index 0000000000000..125ea4e8a854a --- /dev/null +++ b/integration-tests/grpc-cli/src/main/java/io/quarkus/grpc/examples/cli/HelloWorldService.java @@ -0,0 +1,42 @@ +package io.quarkus.grpc.examples.cli; + +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/integration-tests/grpc-cli/src/main/proto/helloworld.proto b/integration-tests/grpc-cli/src/main/proto/helloworld.proto new file mode 100644 index 0000000000000..f20a66ff856d3 --- /dev/null +++ b/integration-tests/grpc-cli/src/main/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/integration-tests/grpc-cli/src/main/resources/application.properties b/integration-tests/grpc-cli/src/main/resources/application.properties new file mode 100644 index 0000000000000..f78754c69f29c --- /dev/null +++ b/integration-tests/grpc-cli/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.grpc.server.use-separate-server=false +quarkus.grpc.server.enable-reflection-service=true diff --git a/integration-tests/grpc-cli/src/test/java/io/quarkus/grpc/cli/GrpcCliTest.java b/integration-tests/grpc-cli/src/test/java/io/quarkus/grpc/cli/GrpcCliTest.java new file mode 100644 index 0000000000000..58bf2f718e216 --- /dev/null +++ b/integration-tests/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() { + 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/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 d41dcca08d040..ea5d5d38ddf6e 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 c798d111640b4..6ae0904fb041d 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) {} } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 487eb8dc9de25..ebffa37fc850d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -384,6 +384,7 @@ grpc-inprocess grpc-vertx grpc-tls + grpc-cli grpc-tls-p12 grpc-plain-text-gzip grpc-plain-text-mutiny