diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 40c2b1e2..89d97c0f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -5,15 +5,14 @@ name: Java CI with Gradle on: push: - branches: [ master ] + branches: + - master pull_request: branches: [ master ] jobs: build: - runs-on: ubuntu-latest - steps: - name: checkout project uses: actions/checkout@v2 @@ -25,4 +24,6 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build ; docker images + env: + GRPC_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: true # otherwise, host name can't be resolved from consul docker image to github runner (it tests) + run: ./gradlew build diff --git a/README.adoc b/README.adoc index b4e03aae..69684767 100644 --- a/README.adoc +++ b/README.adoc @@ -38,7 +38,7 @@ repositories { } dependencies { - compile 'io.github.lognet:grpc-spring-boot-starter:4.5.9' + compile 'io.github.lognet:grpc-spring-boot-starter:4.5.10' } @@ -48,10 +48,10 @@ By default, starter pulls `io.grpc:grpc-netty-shaded` as transitive dependency [source,groovy] ---- - compile ('io.github.lognet:grpc-spring-boot-starter:4.5.8') { + compile ('io.github.lognet:grpc-spring-boot-starter:4.5.10') { exclude group: 'io.grpc', module: 'grpc-netty-shaded' } - compile 'io.grpc:grpc-netty:1.41.0' // <1> + compile 'io.grpc:grpc-netty:1.42.0' // <1> ---- <1> Make sure to pull the version that matches the release. @@ -275,7 +275,7 @@ Their order can also be controlled by below properties : This gives you the ability to set up the desired order of built-in and your custom interceptors. -Error handling interceptor has the highest precedence. +Error handling interceptor has the `Ordered.HIGHEST_PRECEDENCE`. *Keep on reading !!! There is more* @@ -960,6 +960,16 @@ class MyClient{ The starter registers the default implementation of https://github.com/grpc/grpc-java/blob/bab1fe38dc/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java[HealthServiceImpl]. + You can provide you own by registering link:./grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/ManagedHealthStatusService.java[ManagedHealthStatusService] bean in your application context. + +== Spring actuator support + +If you have `org.springframework.boot:spring-boot-starter-actuator` and `org.springframework.boot:spring-boot-starter-web` in the classpath, the starter will expose: + +* `grpc` health indicator under `/actuator/health` endpoint. +* `/actuator/grpc` endpoint. + +This can be controlled by standard https://docs.spring.io/spring-boot/docs/2.5.x/reference/html/actuator.html#actuator.endpoints.enabling[endpoints] and https://docs.spring.io/spring-boot/docs/2.5.x/reference/html/actuator.html#actuator.endpoints.health[health] configuration. + == Consul Integration Starting from version `3.3.0`, the starter will auto-register the running grpc server in Consul registry if `org.springframework.cloud:spring-cloud-starter-consul-discovery` is in classpath and diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c88a2f36..7c178c7b 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,6 @@ | Starter Version | gRPC versions |Spring Boot version | -------------------- |:-------------:|:------------------:| +| [4.5.10](#version-4510)| 1.42.0 |2.5.6 | | [4.5.9](#version-459)| 1.41.0 |2.5.6 | | [4.5.8](#version-458)| 1.41.0 |2.5.0 | | [4.5.7](#version-457)| 1.40.1 |2.5.0 | @@ -28,6 +29,24 @@ | [4.0.0](#version-400)| 1.32.1 |2.3.3.RELEASE | | [3.5.7](#version-357)| 1.31.1 |1.5.13.RELEASE | +# Version 4.5.10 +## :star: New Features + +- Expose grpc health indicator under /actuator/health [#259](https://github.com/LogNet/grpc-spring-boot-starter/issues/259) +- Expose /actuator/grpc endpoint [#258](https://github.com/LogNet/grpc-spring-boot-starter/issues/258) +- kubernetes healthcheck [#98](https://github.com/LogNet/grpc-spring-boot-starter/issues/98) + +## :lady_beetle: Bug Fixes + +- Application with configured grpc.consul.xxx properties fails to start if no consul in classpath [#256](https://github.com/LogNet/grpc-spring-boot-starter/issues/256) + +## :hammer: Dependency Upgrades + +- Upgrade protoc compiler to 3.17.3 [#262](https://github.com/LogNet/grpc-spring-boot-starter/issues/262) +- Upgrade protobuf-gradle-plugin to 0.8.17 [#261](https://github.com/LogNet/grpc-spring-boot-starter/issues/261) +- Upgrade grpc to 1.42.0 [#260](https://github.com/LogNet/grpc-spring-boot-starter/issues/260) + + # Version 4.5.9 ## :star: New Features diff --git a/build.gradle b/build.gradle index 061e526c..0ace023b 100644 --- a/build.gradle +++ b/build.gradle @@ -56,10 +56,11 @@ subprojects { }) - test { - // forkEvery = 1 + tasks.withType(Test) { + //forkEvery = 1 testLogging { exceptionFormat = 'full' + // showStandardStreams = true } } diff --git a/gradle.properties b/gradle.properties index c8bc1331..de860f2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,11 @@ -grpcVersion=1.41.0 +grpcVersion=1.42.0 springBootVersion=2.5.6 springCloudVersion=2020.0.4 gradleErrorPronePluginVersion=2.0.2 errorProneVersion=2.7.1 lombokVersion=1.18.20 -version=4.5.9 +version=4.5.10 group=io.github.lognet description=Spring Boot starter for Google RPC. gitHubUrl=https\://github.com/LogNet/grpc-spring-boot-starter diff --git a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java index 938f96ba..01ace2c0 100644 --- a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java +++ b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java @@ -7,6 +7,7 @@ import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.ReflectionUtils; @@ -16,6 +17,7 @@ @RunWith(SpringRunner.class) @SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE) +@ActiveProfiles("test") public class NoConsulDependencyTest extends GrpcServerTestBase { @Test diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DemoAppTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DemoAppTest.java index 93ae51f0..a10ebd0e 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DemoAppTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DemoAppTest.java @@ -1,21 +1,10 @@ package org.lognet.springboot.grpc; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.grpc.ServerInterceptor; import io.grpc.examples.CalculatorGrpc; import io.grpc.examples.CalculatorOuterClass; import io.grpc.examples.GreeterGrpc; import io.grpc.examples.GreeterOuterClass; -import io.grpc.reflection.v1alpha.ServerReflectionGrpc; -import io.grpc.reflection.v1alpha.ServerReflectionRequest; -import io.grpc.reflection.v1alpha.ServerReflectionResponse; -import io.grpc.reflection.v1alpha.ServiceResponse; -import io.grpc.stub.StreamObserver; -import io.micrometer.prometheus.PrometheusConfig; -import org.awaitility.Awaitility; -import org.hamcrest.Matchers; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,30 +14,15 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.OutputCaptureRule; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.isA; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @@ -58,19 +32,16 @@ */ @RunWith(SpringRunner.class) @SpringBootTest(classes = {DemoApp.class, TestConfig.class}, webEnvironment = RANDOM_PORT - , properties = {"grpc.enableReflection=true", + , properties = { "grpc.shutdownGrace=-1", "spring.main.web-application-type=servlet" }) -@ActiveProfiles({"disable-security", "measure"}) +@ActiveProfiles({"disable-security"}) public class DemoAppTest extends GrpcServerTestBase { - @Autowired - private PrometheusConfig prometheusConfig; - @Autowired - private TestRestTemplate restTemplate; + @Rule public OutputCaptureRule outputCapture = new OutputCaptureRule(); @@ -82,7 +53,7 @@ public class DemoAppTest extends GrpcServerTestBase { @Test - public void disabledServerTest() throws Throwable { + public void disabledServerTest() { assertNotNull(grpcServerRunner); assertNull(grpcInprocessServerRunner); } @@ -110,78 +81,16 @@ public void interceptorsTest() throws ExecutionException, InterruptedException { outputCapture.expect(containsString("I'm not Spring bean interceptor and still being invoked...")); } - @Test - public void actuatorTest() throws ExecutionException, InterruptedException { - ResponseEntity response = restTemplate.getForEntity("/actuator/env", String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - } - @Test public void testDefaultConfigurer() { - Assert.assertEquals("Default configurer should be picked up", - context.getBean(GRpcServerBuilderConfigurer.class).getClass(), - GRpcServerBuilderConfigurer.class); + assertThat(context.getBean(GRpcServerBuilderConfigurer.class),isA(GRpcServerBuilderConfigurer.class)); } - @Test - public void testReflection() throws InterruptedException { - List discoveredServiceNames = new ArrayList<>(); - ServerReflectionRequest request = ServerReflectionRequest.newBuilder().setListServices("services").setHost("localhost").build(); - CountDownLatch latch = new CountDownLatch(1); - ServerReflectionGrpc.newStub(channel).serverReflectionInfo(new StreamObserver() { - @Override - public void onNext(ServerReflectionResponse value) { - List serviceList = value.getListServicesResponse().getServiceList(); - for (ServiceResponse serviceResponse : serviceList) { - discoveredServiceNames.add(serviceResponse.getName()); - } - } - - @Override - public void onError(Throwable t) { - - } - - @Override - public void onCompleted() { - latch.countDown(); - } - }).onNext(request); - - latch.await(3, TimeUnit.SECONDS); - assertFalse(discoveredServiceNames.isEmpty()); - } - - - @Override - protected void afterGreeting() throws Exception { - ResponseEntity metricsResponse = restTemplate.getForEntity("/actuator/metrics", ObjectNode.class); - assertEquals(HttpStatus.OK, metricsResponse.getStatusCode()); - final String metricName = "grpc.server.calls"; - final Optional containsGrpcServerCallsMetric = StreamSupport.stream(Spliterators.spliteratorUnknownSize(metricsResponse.getBody().withArray("names") - .elements(), Spliterator.NONNULL), false) - .map(JsonNode::asText) - .filter(metricName::equals) - .findFirst(); - assertThat("Should contain " + metricName,containsGrpcServerCallsMetric.isPresent()); - Callable getPrometheusMetrics = () -> { - ResponseEntity response = restTemplate.getForEntity("/actuator/prometheus", String.class); - assertEquals(HttpStatus.OK, response.getStatusCode()); - return Stream.of(response.getBody().split(System.lineSeparator())) - .filter(s -> s.contains(metricName.replace('.','_'))) - .count(); - }; - - Awaitility - .waitAtMost(Duration.ofMillis(prometheusConfig.step().toMillis() * 2)) - .until(getPrometheusMetrics,Matchers.greaterThan(0L)); - - } } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java index 42e108db..105e1626 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java @@ -5,9 +5,11 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.examples.GreeterGrpc; import io.grpc.examples.GreeterOuterClass; +import io.grpc.health.v1.HealthGrpc; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -23,7 +25,9 @@ import org.springframework.util.StringUtils; import java.io.IOException; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -59,6 +63,9 @@ public void initialize(GenericApplicationContext applicationContext) { @Autowired protected ApplicationContext context; + @Autowired + protected GRpcServicesRegistry registry; + @Autowired protected GRpcServerProperties gRpcServerProperties; @@ -113,6 +120,13 @@ public void shutdownChannels() { Optional.ofNullable(inProcChannel).ifPresent(ManagedChannel::shutdownNow); } + protected List appServicesNames(){ + return registry.getServiceNameToServiceBeanMap() + .keySet() + .stream() + .filter(name-> !name.equals(HealthGrpc.SERVICE_NAME) && !name.equals(ServerReflectionGrpc.SERVICE_NAME)) + .collect(Collectors.toList()); + } @Test public void simpleGreeting() throws Exception { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java new file mode 100644 index 00000000..a02d600b --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java @@ -0,0 +1,138 @@ +package org.lognet.springboot.grpc.actuator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import io.micrometer.prometheus.PrometheusConfig; +import org.awaitility.Awaitility; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GrpcServerTestBase; +import org.lognet.springboot.grpc.TestConfig; +import org.lognet.springboot.grpc.demo.DemoApp; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.Duration; +import java.util.Optional; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * Created by alexf on 28-Jan-16. + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {DemoApp.class, TestConfig.class}, webEnvironment = RANDOM_PORT + , properties = { + + "management.endpoint.health.show-details=always" + , "management.endpoint.health.show-components=always" + , "spring.main.web-application-type=servlet" +}) +@ActiveProfiles({"disable-security", "measure"}) +public class ActuatorTest extends GrpcServerTestBase { + + @Autowired + private PrometheusConfig prometheusConfig; + + @Autowired + private TestRestTemplate restTemplate; + + + @Test + public void actuatorEnvTest() throws ExecutionException, InterruptedException { + ResponseEntity response = restTemplate.getForEntity("/actuator/env", String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + public void actuatorGrpcTest() throws ExecutionException, InterruptedException { + ResponseEntity response = restTemplate.getForEntity("/actuator/grpc", String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + final DocumentContext json = JsonPath.parse(response.getBody(), Configuration.builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build()); + final String[] statuses = json.read("services.*name", new TypeRef() {}); + assertThat(statuses,Matchers.arrayWithSize(Matchers.greaterThan(0))); + for(String s:statuses) { + assertThat(s, Matchers.not(Matchers.blankOrNullString())); + } + + final Integer port = json.read("port", Integer.class); + assertThat(port,Matchers.greaterThan(0)); + } + + @Test + public void actuatorHealthTest() throws ExecutionException, InterruptedException { + ResponseEntity response = restTemplate.getForEntity("/actuator/health/grpc", String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final DocumentContext json = JsonPath.parse(response.getBody(), Configuration.builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build()); + final TypeRef> setOfString = new TypeRef>() { + }; + final Set services = json.read("components.keys()", setOfString); + assertThat(services,Matchers.containsInAnyOrder( super.appServicesNames().toArray(new String[]{}))); + + final Set statuses = json.read("components.*status", setOfString); + assertThat(statuses,Matchers.contains(Status.UP.getCode())); + + + + } + + @Override + protected void afterGreeting() throws Exception { + + + ResponseEntity metricsResponse = restTemplate.getForEntity("/actuator/metrics", ObjectNode.class); + assertEquals(HttpStatus.OK, metricsResponse.getStatusCode()); + final String metricName = "grpc.server.calls"; + final Optional containsGrpcServerCallsMetric = StreamSupport.stream(Spliterators.spliteratorUnknownSize(metricsResponse.getBody().withArray("names") + .elements(), Spliterator.NONNULL), false) + .map(JsonNode::asText) + .filter(metricName::equals) + .findFirst(); + assertThat("Should contain " + metricName, containsGrpcServerCallsMetric.isPresent()); + + + Callable getPrometheusMetrics = () -> { + ResponseEntity response = restTemplate.getForEntity("/actuator/prometheus", String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + return Stream.of(response.getBody().split(System.lineSeparator())) + .filter(s -> s.contains(metricName.replace('.', '_'))) + .count(); + }; + + Awaitility + .waitAtMost(Duration.ofMillis(prometheusConfig.step().toMillis() * 2)) + .until(getPrometheusMetrics, Matchers.greaterThan(0L)); + + } + + +} diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java index b5c96142..44c1169d 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java @@ -31,6 +31,7 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.context.junit4.SpringRunner; @@ -42,18 +43,18 @@ import static org.junit.Assert.assertTrue; -@SpringBootTest(classes = DemoApp.class,webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(classes = DemoApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @RunWith(SpringRunner.class) @Import({UserDetailsAuthTest.TestCfg.class}) public class UserDetailsAuthTest extends GrpcServerTestBase { @TestConfiguration - static class TestCfg extends GrpcSecurityConfigurerAdapter { + static class TestCfg extends GrpcSecurityConfigurerAdapter { @GRpcService(interceptors = NotSpringBeanInterceptor.class) @Secured({}) - public static class SecuredCalculatorService extends SecuredCalculatorGrpc.SecuredCalculatorImplBase{ + public static class SecuredCalculatorService extends SecuredCalculatorGrpc.SecuredCalculatorImplBase { @Override public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObserver responseObserver) { responseObserver.onNext(DemoAppConfiguration.CalculatorService.calculate(request)); @@ -62,30 +63,33 @@ public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObse } } - static final String pwd="strongPassword1"; - - @Bean - public UserDetails user() { - return User.withDefaultPasswordEncoder() - .username("user1") - .password(pwd) - .roles("reader") - .build(); - } + static final String pwd = "strongPassword1"; - @Override - public void configure(GrpcSecurity builder) throws Exception { - builder.authorizeRequests() - .methods(GreeterGrpc.getSayHelloMethod()).hasAnyRole("reader") - .methods(GreeterGrpc.getSayAuthOnlyHelloMethod()).hasAnyRole("reader") - .methods(CalculatorGrpc.getCalculateMethod()).hasAnyRole("anotherRole") - .withSecuredAnnotation() - .userDetailsService(new InMemoryUserDetailsManager(builder.getApplicationContext().getBean(UserDetails.class))); + @Bean + public static UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(user()); + } + + @Bean + public static UserDetails user() { + return User.withDefaultPasswordEncoder() + .username("user1") + .password(pwd) + .roles("reader") + .build(); + } - } + @Override + public void configure(GrpcSecurity builder) throws Exception { + builder.authorizeRequests() + .methods(GreeterGrpc.getSayHelloMethod()).hasAnyRole("reader") + .methods(GreeterGrpc.getSayAuthOnlyHelloMethod()).hasAnyRole("reader") + .methods(CalculatorGrpc.getCalculateMethod()).hasAnyRole("anotherRole") + .withSecuredAnnotation(); + } } @@ -98,17 +102,15 @@ public void configure(GrpcSecurity builder) throws Exception { public void simpleAuthHeaderFormat() throws ExecutionException, InterruptedException { - final GreeterGrpc.GreeterFutureStub greeterFutureStub = GreeterGrpc.newFutureStub(getChannel(false)); final String reply = greeterFutureStub.sayAuthOnlyHello(Empty.newBuilder().build()).get().getMessage(); - assertNotNull("Reply should not be null",reply); - assertTrue(String.format("Reply should contain name '%s'",user.getUsername()),reply.contains(user.getUsername())); + assertNotNull("Reply should not be null", reply); + assertTrue(String.format("Reply should contain name '%s'", user.getUsername()), reply.contains(user.getUsername())); } - @Test public void shouldFailWithPermissionDenied() { @@ -133,11 +135,11 @@ public void serviceLevelSecurityAuthenticationWithoutAuthorization() { final CalculatorOuterClass.CalculatorResponse response = SecuredCalculatorGrpc .newBlockingStub(selectedChanel)//auth channel .calculate(CalculatorOuterClass.CalculatorRequest.newBuilder() - .setNumber1(1) - .setNumber2(1) - .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) - .build()); - assertThat(response.getResult(),Matchers.is(2d)); + .setNumber1(1) + .setNumber2(1) + .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) + .build()); + assertThat(response.getResult(), Matchers.is(2d)); } @@ -148,10 +150,10 @@ public void shouldFailWithUnauthenticated() { SecuredCalculatorGrpc .newBlockingStub(super.getChannel()) // channel without auth .calculate(CalculatorOuterClass.CalculatorRequest.newBuilder() - .setNumber1(1) - .setNumber2(1) - .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) - .build()); + .setNumber1(1) + .setNumber2(1) + .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) + .build()); }); assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.UNAUTHENTICATED)); @@ -159,13 +161,13 @@ public void shouldFailWithUnauthenticated() { @Override protected Channel getChannel() { - return getChannel(true); + return getChannel(true); } protected Channel getChannel(boolean binaryFormat) { final AuthClientInterceptor interceptor = new AuthClientInterceptor(AuthHeader.builder() - .basic(user.getUsername(),TestCfg.pwd.getBytes()) + .basic(user.getUsername(), TestCfg.pwd.getBytes()) .binaryFormat(binaryFormat) ); return ClientInterceptors.intercept(super.getChannel(), interceptor); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java index f99ee06b..34f9b46a 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java @@ -4,6 +4,15 @@ import io.grpc.health.v1.HealthCheckRequest; import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.reflection.v1alpha.ServiceResponse; +import io.grpc.stub.StreamObserver; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.hamcrest.Matchers; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.GRpcService; @@ -13,26 +22,57 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.InetUtilsProperties; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; +import org.testcontainers.containers.Container; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; @RunWith(SpringRunner.class) -@SpringBootTest(classes = {DemoApp.class, CustomManagedHealthStatusServiceTest.Cfg.class}, webEnvironment = NONE) +@SpringBootTest(classes = {DemoApp.class, CustomManagedHealthStatusServiceTest.Cfg.class}, webEnvironment = NONE, + properties = "grpc.enableReflection=true") @ActiveProfiles("disable-security") - +@Slf4j public class CustomManagedHealthStatusServiceTest extends GrpcServerTestBase { + + @Rule + public GrpcHealthProbeContainer grpcHealthProbe = new GrpcHealthProbeContainer(); + @TestConfiguration - static class Cfg{ + static class Cfg { @GRpcService - static class MyCustomHealthStatusService extends DefaultHealthStatusService {} + static class MyCustomHealthStatusService extends DefaultHealthStatusService { + + + @Getter + private final ArrayList checkedServices = new ArrayList<>(); + + + @Override + public void check(HealthCheckRequest request, StreamObserver responseObserver) { + synchronized (checkedServices) { + checkedServices.add(request.getService()); + } + super.check(request, responseObserver); + } + } } @@ -42,7 +82,53 @@ static class MyCustomHealthStatusService extends DefaultHealthStatusService {} @Test @DirtiesContext public void contextLoads() { - assertThat(healthStatusManager,isA(Cfg.MyCustomHealthStatusService.class)); + assertThat(healthStatusManager, isA(Cfg.MyCustomHealthStatusService.class)); + } + + @Test + @DirtiesContext + public void grpcHealthProbeTest() throws InterruptedException, IOException { + + + Cfg.MyCustomHealthStatusService healthManager = (Cfg.MyCustomHealthStatusService) healthStatusManager; + + final Set registeredServices = healthManager.statuses().keySet(); + assertThat(registeredServices , hasSize(greaterThan(0))); + final List discovered = discoverServicesNames(); + + assertThat(discovered, containsInAnyOrder(registeredServices.toArray())); + + String addressParameter = String.format("-addr=%s:%d", + new InetUtils(new InetUtilsProperties()).findFirstNonLoopbackHostInfo().getIpAddress(), + getPort()); + + + ArrayList allServices = new ArrayList<>(discovered); + allServices.add(""); + + for (String serviceName : allServices) { + final Container.ExecResult execResult = grpcHealthProbe + .execInContainer("/bin/grpc_health_probe", + addressParameter, + "-service=" + serviceName + ); + + assertThat(execResult.getExitCode(), is(0)); + + } + assertThat(healthManager.getCheckedServices(), containsInAnyOrder(allServices.toArray())); + + + final Container.ExecResult execResult = grpcHealthProbe + .execInContainer("/bin/grpc_health_probe", + addressParameter, + "-service=blah" + ); + + assertThat(execResult.getExitCode(), Matchers.not(0)); + + + } @Test @@ -54,7 +140,49 @@ public void testHealthCheck() throws ExecutionException, InterruptedException { assertThat(servingStatus, is(HealthCheckResponse.ServingStatus.SERVING)); - Mockito.verify(healthStatusManager,Mockito.atLeast(1)) - .setStatus(Mockito.any(String.class),Mockito.eq(HealthCheckResponse.ServingStatus.SERVING)); + Mockito.verify(healthStatusManager, Mockito.atLeast(1)) + .setStatus(Mockito.any(String.class), Mockito.eq(HealthCheckResponse.ServingStatus.SERVING)); + } + + @Test + public void testReflection() throws InterruptedException { + + assertThat(discoverServicesNames(), Matchers.not(Matchers.empty())); + } + + private List discoverServicesNames() throws InterruptedException { + List discoveredServiceNames = new ArrayList<>(); + ServerReflectionRequest request = ServerReflectionRequest.newBuilder().setListServices("services").setHost("localhost").build(); + CountDownLatch latch = new CountDownLatch(1); + ServerReflectionGrpc.newStub(channel).serverReflectionInfo(new StreamObserver() { + @Override + public void onNext(ServerReflectionResponse value) { + List serviceList = value.getListServicesResponse().getServiceList(); + for (ServiceResponse serviceResponse : serviceList) { + + final String serviceName = serviceResponse.getName(); + if ( + !serviceName.equals(ServerReflectionGrpc.getServiceDescriptor().getName()) && + !serviceName.equals(HealthGrpc.getServiceDescriptor().getName()) + ) { + + discoveredServiceNames.add(serviceName); + } + } + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + latch.countDown(); + } + }).onNext(request); + + latch.await(3, TimeUnit.SECONDS); + return discoveredServiceNames; } } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/GrpcHealthProbeContainer.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/GrpcHealthProbeContainer.java new file mode 100644 index 00000000..367c6247 --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/GrpcHealthProbeContainer.java @@ -0,0 +1,18 @@ +package org.lognet.springboot.grpc.health; + +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.GenericContainer; + +@Slf4j +public class GrpcHealthProbeContainer extends GenericContainer { + public GrpcHealthProbeContainer() { + super("slyncio/grpc-health-probe"); + withLogConsumer(f -> log.info(f.getUtf8String())) + .withCreateContainerCmdModifier(cmd -> + cmd.withStdinOpen(true) + .withEntrypoint("sh") + ); + } + + +} diff --git a/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml b/grpc-spring-boot-starter-demo/src/test/resources/logback-test-spring.xml similarity index 90% rename from grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml rename to grpc-spring-boot-starter-demo/src/test/resources/logback-test-spring.xml index 081473d9..62692e88 100644 --- a/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml +++ b/grpc-spring-boot-starter-demo/src/test/resources/logback-test-spring.xml @@ -10,4 +10,5 @@ + \ No newline at end of file diff --git a/grpc-spring-boot-starter-gradle-plugin/README.adoc b/grpc-spring-boot-starter-gradle-plugin/README.adoc index 25323151..014727c2 100644 --- a/grpc-spring-boot-starter-gradle-plugin/README.adoc +++ b/grpc-spring-boot-starter-gradle-plugin/README.adoc @@ -23,7 +23,7 @@ Bootstraps the project with `com.google.protobuf` gradle plugin (including `grp ---- plugins { id 'java' - id "io.github.lognet.grpc-spring-boot" version '4.5.9' + id "io.github.lognet.grpc-spring-boot" version '4.5.10' } ---- @@ -53,7 +53,7 @@ grpcSpringBoot { ---- <1> `io.github.lognet:grpc-spring-boot-starter` version to use, defaults to the same version as this plugin. <2> `grpc` version to use, defaults to the version the `io.github.lognet:grpc-spring-boot-starter` was compiled with (see version matrix https://github.com/LogNet/grpc-spring-boot-starter/blob/master/ReleaseNotes.md[here]) -<3> Version of `com.google.protobuf:protoc` protocol compiler to use (defaults to `3.17.2`) +<3> Version of `com.google.protobuf:protoc` protocol compiler to use (defaults to `3.17.3`) The version of `com.google.protobuf` can be controlled via `pluginManagement` block : @@ -67,7 +67,7 @@ pluginManagement { } } ---- -<1> Defaults to `0.8.16` +<1> Defaults to `0.8.17` == License diff --git a/grpc-spring-boot-starter-gradle-plugin/build.gradle b/grpc-spring-boot-starter-gradle-plugin/build.gradle index 8119b30d..d267a509 100644 --- a/grpc-spring-boot-starter-gradle-plugin/build.gradle +++ b/grpc-spring-boot-starter-gradle-plugin/build.gradle @@ -55,7 +55,7 @@ pluginBundle { tags = ['grpc', 'protobuf', 'spring-boot', 'grpc-spring-boot-starter'] } dependencies { - runtime "com.google.protobuf:protobuf-gradle-plugin:0.8.16" + runtime "com.google.protobuf:protobuf-gradle-plugin:0.8.17" } diff --git a/grpc-spring-boot-starter-gradle-plugin/src/main/java/org/lognet/springboot/grpc/gradle/GrpcSpringBootExtension.java b/grpc-spring-boot-starter-gradle-plugin/src/main/java/org/lognet/springboot/grpc/gradle/GrpcSpringBootExtension.java index 6d4da6c0..e3493ba0 100644 --- a/grpc-spring-boot-starter-gradle-plugin/src/main/java/org/lognet/springboot/grpc/gradle/GrpcSpringBootExtension.java +++ b/grpc-spring-boot-starter-gradle-plugin/src/main/java/org/lognet/springboot/grpc/gradle/GrpcSpringBootExtension.java @@ -19,7 +19,7 @@ public GrpcSpringBootExtension(Project project) { protocVersion = this.project.getObjects().property(String.class); - protocVersion.set("3.17.2"); + protocVersion.set("3.17.3"); } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java index 5c3a2997..993168d6 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java @@ -13,18 +13,23 @@ import org.lognet.springboot.grpc.recovery.GRpcExceptionHandlerMethodResolver; import org.lognet.springboot.grpc.recovery.GRpcServiceAdvice; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.convert.converter.Converter; import org.springframework.util.SocketUtils; @@ -42,17 +47,13 @@ @AutoConfigureOrder @AutoConfigureAfter(ValidationAutoConfiguration.class) @ConditionalOnBean(annotation = GRpcService.class) -@EnableConfigurationProperties({GRpcServerProperties.class}) @Import({GRpcValidationConfiguration.class, NettyServerBuilderSelector.class, DefaultHealthStatusService.class }) +@Configuration public class GRpcAutoConfiguration { - - @Autowired - private GRpcServerProperties grpcServerProperties; - @Bean @OnGrpcServerEnabled public GRpcServerRunner grpcServerRunner(@Qualifier("grpcInternalConfigurator") Consumer> configurator, ServerBuilder serverBuilder) { @@ -61,8 +62,8 @@ public GRpcServerRunner grpcServerRunner(@Qualifier("grpcInternalConfigurator") @Bean @ConditionalOnProperty(prefix = "grpc", name = "inProcessServerName") - public GRpcServerRunner grpcInprocessServerRunner(@Qualifier("grpcInternalConfigurator") Consumer> configurator) { - return new GRpcServerRunner(configurator, InProcessServerBuilder.forName(grpcServerProperties.getInProcessServerName())); + public GRpcServerRunner grpcInprocessServerRunner(@Qualifier("grpcInternalConfigurator") Consumer> configurator,GRpcServerProperties gRpcServerProperties) { + return new GRpcServerRunner(configurator, InProcessServerBuilder.forName(gRpcServerProperties.getInProcessServerName())); } @Bean @@ -94,8 +95,27 @@ public GRpcServerBuilderConfigurer serverBuilderConfigurer() { return new GRpcServerBuilderConfigurer(); } + @Bean + public GRpcServerProperties gRpcServerProperties(){ + return new GRpcServerProperties(); + } + + @ConditionalOnMissingClass("org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties") + @Bean + public ConfigurationPropertiesBindHandlerAdvisor skipConsulDiscoveryBinding( ){ + return bindHandler-> new AbstractBindHandler(bindHandler) { + private final ConfigurationPropertyName grpcConsulConfigProperty = ConfigurationPropertyName.of("grpc.consul"); + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { + // otherwise, it will try to instantiate grpc.consul property and discovery field class doesn't exist + return grpcConsulConfigProperty.equals(name)? null : super.onStart(name, target, context); + } + } ; + } + + @Bean(name = "grpcInternalConfigurator") - public Consumer> configurator(GRpcServerBuilderConfigurer configurer) { + public Consumer> configurator(GRpcServerBuilderConfigurer configurer,GRpcServerProperties grpcServerProperties) { return serverBuilder -> { if (grpcServerProperties.isEnabled()) { Optional.ofNullable(grpcServerProperties.getSecurity()) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java new file mode 100644 index 00000000..8e877308 --- /dev/null +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java @@ -0,0 +1,159 @@ +package org.lognet.springboot.grpc.autoconfigure.actuate; + +import io.grpc.BindableService; +import io.grpc.ServiceDescriptor; +import io.grpc.health.v1.HealthCheckResponse; +import io.grpc.health.v1.HealthGrpc; +import lombok.Builder; +import lombok.Getter; +import org.lognet.springboot.grpc.GRpcServicesRegistry; +import org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration; +import org.lognet.springboot.grpc.autoconfigure.OnGrpcServerEnabled; +import org.lognet.springboot.grpc.context.LocalRunningGrpcPort; +import org.lognet.springboot.grpc.health.ManagedHealthStatusService; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.CompositeHealthContributor; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@ConditionalOnClass(HealthContributor.class) +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(GRpcAutoConfiguration.class) +@OnGrpcServerEnabled +public class GRpcActuateAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnEnabledHealthIndicator("grpc") + static class GRpcHealthHealthContributorConfiguration { + @Bean + @ConditionalOnMissingBean(name = "grpcHealthIndicator") + public HealthContributor grpcHealthIndicator(GRpcServicesRegistry registry, ManagedHealthStatusService healthStatusService) { + final Map services = registry.getServiceNameToServiceBeanMap() + .keySet() + .stream() + .filter(s -> !HealthGrpc.SERVICE_NAME.equals(s)) + .collect(Collectors.toMap(Function.identity(), s -> new AbstractHealthIndicator() { + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + final HealthCheckResponse.ServingStatus status = healthStatusService.statuses().get(s); + if (null == status) { + builder.unknown(); + return; + } + switch (status) { + case SERVING: + builder.up(); + break; + case NOT_SERVING: + builder.down(); + break; + case UNKNOWN: + case UNRECOGNIZED: + builder.unknown(); + break; + case SERVICE_UNKNOWN: + builder.outOfService(); + break; + + } + } + })); + return CompositeHealthContributor.fromMap(services); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnAvailableEndpoint(endpoint = GrpcEndpoint.class) + static class GrpcEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean + public GrpcEndpoint grpcEndpoint(GRpcServicesRegistry registry, @LocalRunningGrpcPort int port) { + return new GrpcEndpoint(registry, port); + } + + } + + @Endpoint(id = "grpc") + static class GrpcEndpoint { + public static final class GRpcServices { + + @Builder + @Getter + static class GrpcService{ + private String name; + private List methods; + } + @Builder + @Getter + static class GrpcMethod{ + private String name; + } + @Getter + private final List services; + + @Getter + int port; + + private GRpcServices(Map services, int port) { + this.port = port; + + this.services = services + .values() + .stream() + .map( + s -> { + final ServiceDescriptor serviceDescriptor = s.bindService().getServiceDescriptor(); + return GrpcService.builder() + .name(serviceDescriptor.getName()) + .methods(serviceDescriptor.getMethods() + .stream() + .map(m->GrpcMethod.builder() + .name(m.getBareMethodName()) + .build() + ) + .collect(Collectors.toList()) + ) + .build(); + + + } + ).collect(Collectors.toList()); + } + } + + + private final GRpcServicesRegistry registry; + private int port; + + public GrpcEndpoint(GRpcServicesRegistry registry, int port) { + this.registry = registry; + this.port = port; + } + + + + @ReadOperation + public GRpcServices services() { + return new GRpcServices(registry.getBeanNameToServiceBeanMap(),port); + } + } + + +} diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/context/LocalRunningGrpcPort.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/context/LocalRunningGrpcPort.java index b1bedddf..d41d01f8 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/context/LocalRunningGrpcPort.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/context/LocalRunningGrpcPort.java @@ -2,12 +2,16 @@ import org.springframework.beans.factory.annotation.Value; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented -@Value("#{@'grpc-org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties'.getRunningPort()}") +@Value("#{@gRpcServerProperties.getRunningPort()}") public @interface LocalRunningGrpcPort { } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/DefaultHealthStatusService.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/DefaultHealthStatusService.java index 0014f4a7..2ff5d92e 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/DefaultHealthStatusService.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/DefaultHealthStatusService.java @@ -9,13 +9,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Configuration; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + @GRpcService @Configuration @ConditionalOnMissingBean(ManagedHealthStatusService.class) public class DefaultHealthStatusService extends ManagedHealthStatusService { private final HealthStatusManager healthStatusManager = new HealthStatusManager(); private final HealthGrpc.HealthImplBase service = (HealthGrpc.HealthImplBase) healthStatusManager.getHealthService(); - + private final Map statusMap = new ConcurrentHashMap<>(); + private final Map unmodifiableStatusMap = Collections.unmodifiableMap(statusMap); @Override public void onShutdown() { healthStatusManager.enterTerminalState(); @@ -23,9 +28,15 @@ public void onShutdown() { @Override public void setStatus(String service, HealthCheckResponse.ServingStatus status) { + statusMap.put(service,status); healthStatusManager.setStatus(service,status); } + @Override + public Map statuses() { + return unmodifiableStatusMap; + } + @Override public void check(HealthCheckRequest request, StreamObserver responseObserver) { service.check(request, responseObserver); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/ManagedHealthStatusService.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/ManagedHealthStatusService.java index 3858c26c..e3f94d64 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/ManagedHealthStatusService.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/health/ManagedHealthStatusService.java @@ -3,6 +3,8 @@ import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthGrpc; +import java.util.Map; + public abstract class ManagedHealthStatusService extends HealthGrpc.HealthImplBase{ /** @@ -17,6 +19,7 @@ public abstract class ManagedHealthStatusService extends HealthGrpc.HealthImplBa */ public abstract void setStatus(String service, HealthCheckResponse.ServingStatus status); + public abstract Map statuses(); } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java index 3a2911f4..c16c2044 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java @@ -44,7 +44,7 @@ public class GrpcSecurityConfiguration { private GrpcSecurity grpcSecurity; @Bean - public BeanPostProcessor bypassMethodInterceptorForGrpcMethodInvocation(){ + public static BeanPostProcessor bypassMethodInterceptorForGrpcMethodInvocation(){ return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { diff --git a/grpc-spring-boot-starter/src/main/resources/META-INF/spring.factories b/grpc-spring-boot-starter/src/main/resources/META-INF/spring.factories index 4eb35eaa..37fe237b 100644 --- a/grpc-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/grpc-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -2,5 +2,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration,\ org.lognet.springboot.grpc.autoconfigure.metrics.GRpcMetricsAutoConfiguration,\ org.lognet.springboot.grpc.autoconfigure.consul.ConsulGrpcAutoConfiguration,\ -org.lognet.springboot.grpc.autoconfigure.security.SecurityAutoConfiguration +org.lognet.springboot.grpc.autoconfigure.security.SecurityAutoConfiguration,\ +org.lognet.springboot.grpc.autoconfigure.actuate.GRpcActuateAutoConfiguration