diff --git a/docs/src/main/asciidoc/grpc-service-implementation.adoc b/docs/src/main/asciidoc/grpc-service-implementation.adoc index 3f9c3bf7e20b2..f3dd6fa72a6fa 100644 --- a/docs/src/main/asciidoc/grpc-service-implementation.adoc +++ b/docs/src/main/asciidoc/grpc-service-implementation.adoc @@ -203,6 +203,8 @@ This service allows tools like https://github.com/fullstorydev/grpcurl[grpcurl] The reflection service is enabled by default in _dev_ mode. In test or production mode, you need to enable it explicitly by setting `quarkus.grpc.server.enable-reflection-service` to `true`. +NOTE: Quarkus exposes both the reflection service `v1` and `v1alpha`. + == Scaling By default, quarkus-grpc starts a single gRPC server running on a single event loop. diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/MultipleStubsInjectionTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/MultipleStubsInjectionTest.java index 25b4569c7faf1..fff2045986c74 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/MultipleStubsInjectionTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/MultipleStubsInjectionTest.java @@ -13,8 +13,18 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.grpc.Channel; -import io.grpc.examples.goodbyeworld.*; -import io.grpc.examples.helloworld.*; +import io.grpc.examples.goodbyeworld.FarewellGrpc; +import io.grpc.examples.goodbyeworld.GoodbyeReply; +import io.grpc.examples.goodbyeworld.GoodbyeReplyOrBuilder; +import io.grpc.examples.goodbyeworld.GoodbyeRequest; +import io.grpc.examples.goodbyeworld.GoodbyeRequestOrBuilder; +import io.grpc.examples.goodbyeworld.MutinyFarewellGrpc; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloReplyOrBuilder; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.examples.helloworld.HelloRequestOrBuilder; +import io.grpc.examples.helloworld.MutinyGreeterGrpc; import io.quarkus.grpc.GrpcClient; import io.quarkus.grpc.server.services.GoodbyeService; import io.quarkus.grpc.server.services.HelloService; diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcReflectionAlphaTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcReflectionAlphaTest.java new file mode 100644 index 0000000000000..6fc1f65a1c7ee --- /dev/null +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcReflectionAlphaTest.java @@ -0,0 +1,296 @@ +package io.quarkus.grpc.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Flow; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.google.protobuf.ByteString; + +import grpc.health.v1.HealthGrpc; +import grpc.reflection.v1alpha.MutinyServerReflectionGrpc; +import grpc.reflection.v1alpha.Reflection; +import io.grpc.Status; +import io.grpc.reflection.testing.MutinyReflectableServiceGrpc; +import io.grpc.reflection.testing.ReflectionTestDepthThreeProto; +import io.grpc.reflection.testing.ReflectionTestDepthTwoProto; +import io.grpc.reflection.testing.ReflectionTestProto; +import io.grpc.reflection.testing.Reply; +import io.grpc.reflection.testing.Request; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.grpc.GrpcService; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor; + +/** + * Check the behavior of the reflection service (v1alpha) + */ +public class GrpcReflectionAlphaTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(HealthGrpc.class.getPackage()) + .addPackage(MutinyReflectableServiceGrpc.class.getPackage()) + .addClass(MyReflectionService.class)) + .setFlatClassPath(true) + .withConfigurationResource("reflection-config.properties"); + + @GrpcClient("reflection-service") + MutinyServerReflectionGrpc.MutinyServerReflectionStub reflection; + + private UnicastProcessor processor; + private ResettableSubscriber subscriber; + + @BeforeEach + public void setUp() { + processor = UnicastProcessor.create(); + subscriber = new ResettableSubscriber<>(); + } + + @AfterEach + public void cleanUp() { + processor.onComplete(); + subscriber.cancel(); + } + + @Test + public void testRetrievingListOfServices() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder().setHost("localhost") + .setListServices("").build(); + + Reflection.ServerReflectionResponse response = invoke(request); + List list = response.getListServicesResponse().getServiceList(); + assertThat(list).hasSize(2) + .anySatisfy(r -> assertThat(r.getName()).isEqualTo("grpc.reflection.testing.ReflectableService")) + .anySatisfy(r -> assertThat(r.getName()).isEqualTo("grpc.health.v1.Health")); + } + + @Test + public void testRetrievingFilesByFileName() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileByFilename("reflection/reflection_test_depth_three.proto") + .build(); + + Reflection.ServerReflectionResponse expected = Reflection.ServerReflectionResponse.newBuilder() + .setValidHost("localhost") + .setOriginalRequest(request) + .setFileDescriptorResponse( + Reflection.FileDescriptorResponse.newBuilder() + .addFileDescriptorProto( + ReflectionTestDepthThreeProto.getDescriptor().toProto().toByteString()) + .build()) + .build(); + + Reflection.ServerReflectionResponse response = invoke(request); + assertThat(response).isEqualTo(expected); + } + + @Test + public void testRetrievingFilesByFileNameWithUnknownFileName() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileByFilename("reflection/unknown.proto") + .build(); + + Reflection.ServerReflectionResponse response = invoke(request); + assertThat(response.getErrorResponse().getErrorCode()).isEqualTo(Status.Code.NOT_FOUND.value()); + } + + private Reflection.ServerReflectionResponse invoke(Reflection.ServerReflectionRequest request) { + subscriber.reset(); + Multi multi = reflection.serverReflectionInfo(processor); + multi.subscribe().withSubscriber(subscriber); + subscriber.awaitForSubscription(); + processor.onNext(request); + return subscriber.awaitAndGetLast(); + } + + @Test + public void testRetrievingFilesContainingSymbol() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileContainingSymbol("grpc.reflection.testing.ReflectableService.Method") + .build(); + + List responses = Arrays.asList( + ReflectionTestProto.getDescriptor().toProto().toByteString(), + ReflectionTestDepthTwoProto.getDescriptor().toProto().toByteString(), + ReflectionTestDepthThreeProto.getDescriptor().toProto().toByteString()); + + Reflection.ServerReflectionResponse response = invoke(request); + List list = response.getFileDescriptorResponse().getFileDescriptorProtoList(); + assertThat(list).containsExactlyInAnyOrderElementsOf(responses); + } + + @Test + public void testRetrievingFilesContainingUnknownSymbol() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileContainingSymbol("grpc.reflection.testing.ReflectableService.UnknownMethod") + .build(); + + Reflection.ServerReflectionResponse response = invoke(request); + List list = response.getFileDescriptorResponse().getFileDescriptorProtoList(); + assertThat(list).isEmpty(); + assertThat(response.getErrorResponse().getErrorMessage()).contains("UnknownMethod"); + assertThat(response.getErrorResponse().getErrorCode()).isEqualTo(Status.Code.NOT_FOUND.value()); + } + + @Test + public void testRetrievingFilesContainingNestedSymbol() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileContainingSymbol("grpc.reflection.testing.NestedTypeOuter.Middle.Inner") + .build(); + Reflection.ServerReflectionResponse expected = Reflection.ServerReflectionResponse.newBuilder() + .setValidHost("localhost") + .setOriginalRequest(request) + .setFileDescriptorResponse( + Reflection.FileDescriptorResponse.newBuilder() + .addFileDescriptorProto( + ReflectionTestDepthThreeProto.getDescriptor().toProto().toByteString()) + .build()) + .build(); + Reflection.ServerReflectionResponse resp = invoke(request); + assertThat(resp).isEqualTo(expected); + } + + @Test + public void testRetrievingFilesContainingExtension() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileContainingExtension( + Reflection.ExtensionRequest.newBuilder() + .setContainingType("grpc.reflection.testing.ThirdLevelType") + .setExtensionNumber(100) + .build()) + .build(); + + List expected = Arrays.asList( + ReflectionTestProto.getDescriptor().toProto().toByteString(), + ReflectionTestDepthTwoProto.getDescriptor().toProto().toByteString(), + ReflectionTestDepthThreeProto.getDescriptor().toProto().toByteString()); + + Reflection.ServerReflectionResponse response = invoke(request); + assertThat(response.getFileDescriptorResponse().getFileDescriptorProtoList()) + .containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + public void testRetrievingFilesContainingNestedExtension() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setFileContainingExtension( + Reflection.ExtensionRequest.newBuilder() + .setContainingType("grpc.reflection.testing.ThirdLevelType") + .setExtensionNumber(101) + .build()) + .build(); + + Reflection.ServerReflectionResponse expected = Reflection.ServerReflectionResponse.newBuilder() + .setValidHost("localhost") + .setOriginalRequest(request) + .setFileDescriptorResponse( + Reflection.FileDescriptorResponse.newBuilder() + .addFileDescriptorProto( + ReflectionTestDepthTwoProto.getDescriptor().toProto().toByteString()) + .addFileDescriptorProto( + ReflectionTestDepthThreeProto.getDescriptor().toProto().toByteString()) + .build()) + .build(); + + Reflection.ServerReflectionResponse response = invoke(request); + assertThat(response).isEqualTo(expected); + } + + @Test + public void testRetrievingAllExtensionNumbersOfType() { + Reflection.ServerReflectionRequest request = Reflection.ServerReflectionRequest.newBuilder() + .setHost("localhost") + .setAllExtensionNumbersOfType("grpc.reflection.testing.ThirdLevelType") + .build(); + + List expected = Arrays.asList(100, 101); + + Reflection.ServerReflectionResponse response = invoke(request); + List list = response.getAllExtensionNumbersResponse().getExtensionNumberList(); + assertThat(list).containsExactlyInAnyOrderElementsOf(expected); + } + + private static class ResettableSubscriber implements Flow.Subscriber { + + private Flow.Subscription subscription; + private volatile T last; + private boolean completed; + private Throwable failure; + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + } + + public void reset() { + this.subscription = null; + } + + public void awaitForSubscription() { + await().until(() -> subscription != null); + } + + public T awaitAndGetLast() { + validate(); + last = null; + subscription.request(1); + await().until(() -> last != null); + return last; + } + + @Override + public void onNext(T t) { + last = t; + } + + @Override + public void onError(Throwable throwable) { + this.failure = throwable; + } + + @Override + public void onComplete() { + this.completed = true; + } + + private void validate() { + if (this.failure != null || this.completed) { + throw new IllegalStateException("Subscriber already in a terminal state"); + } + } + + public void cancel() { + this.subscription.cancel(); + } + } + + @GrpcService + public static class MyReflectionService extends MutinyReflectableServiceGrpc.ReflectableServiceImplBase { + @Override + public Uni method(Request request) { + String message = request.getMessage(); + return Uni.createFrom().item(Reply.newBuilder().setMessage(message).build()); + } + } + +} diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/FailingInInterceptorTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/FailingInInterceptorTest.java index 35e46d90cf292..b48a5e149bc21 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/FailingInInterceptorTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/FailingInInterceptorTest.java @@ -18,7 +18,11 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; -import io.grpc.examples.helloworld.*; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; import io.quarkus.grpc.GlobalInterceptor; import io.quarkus.grpc.GrpcClient; import io.quarkus.grpc.server.services.HelloService; diff --git a/extensions/grpc/deployment/src/test/resources/reflection-config.properties b/extensions/grpc/deployment/src/test/resources/reflection-config.properties index fb8074812b64e..be2f47cdb7aa3 100644 --- a/extensions/grpc/deployment/src/test/resources/reflection-config.properties +++ b/extensions/grpc/deployment/src/test/resources/reflection-config.properties @@ -1,3 +1,7 @@ +quarkus.grpc.server.enable-reflection-service=true + quarkus.grpc.clients.reflection-service.host=localhost quarkus.grpc.clients.reflection-service.port=9001 -quarkus.grpc.server.enable-reflection-service=true \ No newline at end of file + +quarkus.grpc.clients.reflection-service-alpha.host=localhost +quarkus.grpc.clients.reflection-service-alpha.port=9001 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 48f4d4b39f254..010b9c442bbd3 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 @@ -47,7 +47,8 @@ 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.ReflectionService; +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; @@ -162,10 +163,15 @@ private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration, if (reflectionServiceEnabled) { LOGGER.info("Registering gRPC reflection service"); - ReflectionService reflectionService = new ReflectionService(definitions); - ServerServiceDefinition serviceDefinition = ServerInterceptors.intercept(reflectionService, globalInterceptors); + ReflectionServiceV1 reflectionServiceV1 = new ReflectionServiceV1(definitions); + ReflectionServiceV1alpha reflectionServiceV1alpha = new ReflectionServiceV1alpha(definitions); + ServerServiceDefinition serviceDefinition = ServerInterceptors.intercept(reflectionServiceV1, globalInterceptors); GrpcServiceBridge bridge = GrpcServiceBridge.bridge(serviceDefinition); bridge.bind(server); + ServerServiceDefinition serviceDefinitionAlpha = ServerInterceptors.intercept(reflectionServiceV1alpha, + globalInterceptors); + GrpcServiceBridge bridgeAlpha = GrpcServiceBridge.bridge(serviceDefinitionAlpha); + bridgeAlpha.bind(server); } initHealthStorage(); @@ -386,7 +392,7 @@ private void devModeReload(GrpcContainer grpcContainer, Vertx vertx, GrpcServerC definitions.add(service.definition); } - ServerServiceDefinition reflectionService = new ReflectionService(definitions).bindService(); + ServerServiceDefinition reflectionService = new ReflectionServiceV1(definitions).bindService(); for (ServerMethodDefinition method : reflectionService.getMethods()) { methods.put(method.getMethodDescriptor().getFullMethodName(), method); @@ -491,7 +497,8 @@ private Map.Entry buildServer(Vertx vertx, GrpcServerConfigurat if (reflectionServiceEnabled) { LOGGER.info("Registering gRPC reflection service"); - builder.addService(new ReflectionService(definitions)); + builder.addService(new ReflectionServiceV1(definitions)); + builder.addService(new ReflectionServiceV1alpha(definitions)); } for (ServerInterceptor serverInterceptor : grpcContainer.getSortedGlobalInterceptors()) { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcConfiguration.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcConfiguration.java index 9d6096960b5c5..1cc2406f666c6 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcConfiguration.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcConfiguration.java @@ -2,7 +2,11 @@ import java.util.Map; -import io.quarkus.runtime.annotations.*; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; /** * gRPC configuration root. diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionService.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java similarity index 91% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionService.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java index 47b9892028cf7..0f57ad617c4f0 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionService.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/reflection/ReflectionServiceV1.java @@ -24,11 +24,11 @@ import io.grpc.reflection.v1.ServiceResponse; import io.smallrye.mutiny.Multi; -public class ReflectionService extends MutinyServerReflectionGrpc.ServerReflectionImplBase { +public class ReflectionServiceV1 extends MutinyServerReflectionGrpc.ServerReflectionImplBase { private final GrpcServerIndex index; - public ReflectionService(List definitions) { + public ReflectionServiceV1(List definitions) { index = new GrpcServerIndex(definitions); } @@ -40,17 +40,17 @@ public Multi serverReflectionInfo(Multi definitions) { + index = new GrpcServerIndex(definitions); + } + + @Override + public Multi serverReflectionInfo(Multi request) { + return request + .onItem().transform(new Function() { + @Override + public Reflection.ServerReflectionResponse apply(Reflection.ServerReflectionRequest req) { + switch (req.getMessageRequestCase()) { + case LIST_SERVICES: + return ReflectionServiceV1alpha.this.getServiceList(req); + case FILE_BY_FILENAME: + return ReflectionServiceV1alpha.this.getFileByName(req); + case FILE_CONTAINING_SYMBOL: + return ReflectionServiceV1alpha.this.getFileContainingSymbol(req); + case FILE_CONTAINING_EXTENSION: + return ReflectionServiceV1alpha.this.getFileByExtension(req); + case ALL_EXTENSION_NUMBERS_OF_TYPE: + return ReflectionServiceV1alpha.this.getAllExtensions(req); + default: + return ReflectionServiceV1alpha.this.getErrorResponse(req, Status.Code.UNIMPLEMENTED, + "not implemented " + req.getMessageRequestCase()); + + } + } + }); + } + + private Reflection.ServerReflectionResponse getServiceList(Reflection.ServerReflectionRequest request) { + Reflection.ListServiceResponse response = index.getServiceNames().stream() + .map(new Function() { // NOSONAR + @Override + public Reflection.ServiceResponse apply(String s) { + return Reflection.ServiceResponse.newBuilder().setName(s).build(); + } + }) + .collect(new Supplier() { + @Override + public Reflection.ListServiceResponse.Builder get() { + return Reflection.ListServiceResponse.newBuilder(); + } + }, + new BiConsumer() { + @Override + public void accept(Reflection.ListServiceResponse.Builder builder, + Reflection.ServiceResponse value) { + builder.addService(value); + } + }, + new BiConsumer() { // NOSONAR + @Override + public void accept(Reflection.ListServiceResponse.Builder b1, + Reflection.ListServiceResponse.Builder b2) { + b1.addAllService(b2.getServiceList()); + } + }) + .build(); + + return Reflection.ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setListServicesResponse(response) + .build(); + } + + private Reflection.ServerReflectionResponse getFileByName(Reflection.ServerReflectionRequest request) { + String name = request.getFileByFilename(); + FileDescriptor fd = index.getFileDescriptorByName(name); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "File not found (" + name + ")"); + } + } + + private Reflection.ServerReflectionResponse getFileContainingSymbol(Reflection.ServerReflectionRequest request) { + String symbol = request.getFileContainingSymbol(); + FileDescriptor fd = index.getFileDescriptorBySymbol(symbol); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "Symbol not found (" + symbol + ")"); + } + } + + private Reflection.ServerReflectionResponse getFileByExtension(Reflection.ServerReflectionRequest request) { + Reflection.ExtensionRequest extensionRequest = request.getFileContainingExtension(); + String type = extensionRequest.getContainingType(); + int extension = extensionRequest.getExtensionNumber(); + FileDescriptor fd = index.getFileDescriptorByExtensionAndNumber(type, extension); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, + "Extension not found (" + type + ", " + extension + ")"); + } + } + + private Reflection.ServerReflectionResponse getAllExtensions(Reflection.ServerReflectionRequest request) { + String type = request.getAllExtensionNumbersOfType(); + Set extensions = index.getExtensionNumbersOfType(type); + if (extensions != null) { + Reflection.ExtensionNumberResponse.Builder builder = Reflection.ExtensionNumberResponse.newBuilder() + .setBaseTypeName(type) + .addAllExtensionNumber(extensions); + return Reflection.ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setAllExtensionNumbersResponse(builder) + .build(); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "Type not found."); + } + } + + private Reflection.ServerReflectionResponse getServerReflectionResponse( + Reflection.ServerReflectionRequest request, FileDescriptor fd) { + Reflection.FileDescriptorResponse.Builder fdRBuilder = Reflection.FileDescriptorResponse.newBuilder(); + + // Traverse the descriptors to get the full list of dependencies and add them to the builder + Set seenFiles = new HashSet<>(); + Queue frontier = new ArrayDeque<>(); + seenFiles.add(fd.getName()); + frontier.add(fd); + while (!frontier.isEmpty()) { + FileDescriptor nextFd = frontier.remove(); + fdRBuilder.addFileDescriptorProto(nextFd.toProto().toByteString()); + for (FileDescriptor dependencyFd : nextFd.getDependencies()) { + if (!seenFiles.contains(dependencyFd.getName())) { + seenFiles.add(dependencyFd.getName()); + frontier.add(dependencyFd); + } + } + } + return Reflection.ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setFileDescriptorResponse(fdRBuilder) + .build(); + } + + private Reflection.ServerReflectionResponse getErrorResponse( + Reflection.ServerReflectionRequest request, Status.Code code, String message) { + return Reflection.ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setErrorResponse( + Reflection.ErrorResponse.newBuilder() + .setErrorCode(code.value()) + .setErrorMessage(message)) + .build(); + + } + +} diff --git a/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/README.md b/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/README.md new file mode 100644 index 0000000000000..411a672d56314 --- /dev/null +++ b/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/README.md @@ -0,0 +1,2 @@ +While the v1alpha is deprecated since 2018 (https://github.com/grpc/grpc-proto/commits/master/grpc/reflection/v1/reflection.proto), +most tools are still using it. So we keep it around. \ No newline at end of file diff --git a/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/reflection.proto b/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/reflection.proto new file mode 100644 index 0000000000000..462e85a222ae2 --- /dev/null +++ b/extensions/grpc/stubs/src/main/proto/reflection/v1alpha/reflection.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} \ No newline at end of file