diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 100ad90fe3abe..860fbd9d39e11 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -1281,6 +1281,7 @@ quarkus.kubernetes.env-vars.foo.configmap=myconfigmap `quarkus.kubernetes.env-vars` are deprecated (though still currently supported as of this writing) and the new declaration style should be used instead. See <<#env-vars>> and more specifically <> for more details. +[[deployment]] == Deployment To trigger building and deploying a container image you need to enable the `quarkus.kubernetes.deploy` flag (the flag is disabled by default - furthermore it has no effect during test runs or dev mode). diff --git a/docs/src/main/asciidoc/grpc-kubernetes.adoc b/docs/src/main/asciidoc/grpc-kubernetes.adoc new file mode 100644 index 0000000000000..c089aa58ede4f --- /dev/null +++ b/docs/src/main/asciidoc/grpc-kubernetes.adoc @@ -0,0 +1,81 @@ +//// +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 +//// += Deploying your gRPC Service in Kubernetes +include::_attributes.adoc[] +:categories: serialization +:summary: This guide explains how to deploy your gRPC services in Quarkus to Kubernetes. + +This page explains how to deploy your gRPC service in Quarkus in Kubernetes. +We'll continue with the example from xref:grpc-getting-started.adoc[the Getting Started gRPC guide]. + +== Configuring your project to use the Quarkus Kubernetes extension + +Edit the `pom.xml` file to add the Quarkus Kubernetes extension dependency (just under ``): + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-kubernetes + +---- + +Next, we want to expose our application using the Kubernetes Ingress resource: + +[source,properties] +---- +quarkus.kubernetes.ingress.expose=true +---- + +By default, the Quarkus Kubernetes will expose the HTTP port with name `http` which is bound to the Quarkus HTTP server, not the gRPC service. To expose the gRPC server instead, set the `quarkus.kubernetes.ingress.target-port=grpc` property in your application.properties: + +[source,properties] +---- +quarkus.kubernetes.ingress.target-port=grpc +---- + +Finally, we need to generate the Kubernetes manifests by running the Maven command in a terminal: + +[source,shell] +---- +$ mvn clean package +---- + +Once generated, you can look at the `target/kubernetes` directory: + +[source,txt] +---- +target/kubernetes +└── kubernetes.json +└── kubernetes.yml +---- + +You can find more information about how to deploy the application in Kubernetes in the xref:deploying-to-kubernetes.adoc#deployment[the Kubernetes guide]. + +== Using gRPC Health probes + +By default, the Kubernetes resources do not contain readiness and liveness probes. To add them, edit the `pom.xml` to add the Smallrye Health extension: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-health + +---- + +TIP: More information about the health extension can be found in xref:microprofile-health.adoc[the Microprofile Health guide]. + +By the default, this extension will configure the probes to use the HTTP server (which is provided by some extensions like the Quarkus RESTEasy reactive extension). Internally, this probe will also use xref:grpc-service-implementation.adoc#health[the generated gRPC Health services]. + +If your application does not use any Quarkus extension that exposes an HTTP server, you can still configure the probes to directly use the gRPC Health service by adding the property `quarkus.kubernetes.readiness-probe.grpc-action-enabled=true` into your configuration: + +[source,properties] +---- +quarkus.kubernetes.readiness-probe.grpc-action-enabled=true +---- \ No newline at end of file diff --git a/docs/src/main/asciidoc/grpc-service-implementation.adoc b/docs/src/main/asciidoc/grpc-service-implementation.adoc index f3dd6fa72a6fa..8980970ac6181 100644 --- a/docs/src/main/asciidoc/grpc-service-implementation.adoc +++ b/docs/src/main/asciidoc/grpc-service-implementation.adoc @@ -158,6 +158,7 @@ public class StreamingService implements Streaming { } ---- +[[health]] == Health Check For the implemented services, Quarkus gRPC exposes health information in the following format: [source,protobuf] diff --git a/docs/src/main/asciidoc/grpc.adoc b/docs/src/main/asciidoc/grpc.adoc index 4ea3b9fa56aa1..7e6af77c5b405 100644 --- a/docs/src/main/asciidoc/grpc.adoc +++ b/docs/src/main/asciidoc/grpc.adoc @@ -30,4 +30,5 @@ Quarkus gRPC is based on https://vertx.io/docs/vertx-grpc/java/[Vert.x gRPC]. * xref:grpc-getting-started.adoc[Getting Started] * xref:grpc-service-implementation.adoc[Implementing a gRPC Service] * xref:grpc-service-consumption.adoc[Consuming a gRPC Service] +* xref:grpc-kubernetes.adoc[Deploying your gRPC Service in Kubernetes] * xref:grpc-xds.adoc[Enabling xDS gRPC support] diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerConverter.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerConverter.java index 6e09d1cf91ca0..6e11dd995851c 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerConverter.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerConverter.java @@ -20,10 +20,10 @@ private static ContainerBuilder convert(String name, ContainerConfig c) { c.command.ifPresent(w -> b.withCommand(w.toArray(new String[0]))); c.arguments.ifPresent(w -> b.withArguments(w.toArray(new String[0]))); if (c.readinessProbe != null && c.readinessProbe.hasUserSuppliedAction()) { - b.withReadinessProbe(ProbeConverter.convert(c.readinessProbe)); + b.withReadinessProbe(ProbeConverter.convert(name, c.readinessProbe)); } if (c.livenessProbe != null && c.livenessProbe.hasUserSuppliedAction()) { - b.withLivenessProbe(ProbeConverter.convert(c.livenessProbe)); + b.withLivenessProbe(ProbeConverter.convert(name, c.livenessProbe)); } b.addAllToEnvVars(c.convertToEnvs()); c.ports.entrySet().forEach(e -> b.addToPorts(PortConverter.convert(e))); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 591ae9947c4a3..740f117568357 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -985,10 +985,11 @@ private static Optional createLivenessProbe(String name, Str Optional livenessPath) { if (livenessProbe.hasUserSuppliedAction()) { return Optional.of( - new DecoratorBuildItem(target, new AddLivenessProbeDecorator(name, ProbeConverter.convert(livenessProbe)))); + new DecoratorBuildItem(target, + new AddLivenessProbeDecorator(name, ProbeConverter.convert(name, livenessProbe)))); } else if (livenessPath.isPresent()) { return Optional.of(new DecoratorBuildItem(target, new AddLivenessProbeDecorator(name, - ProbeConverter.builder(livenessProbe).withHttpActionPath(livenessPath.get().getPath()).build()))); + ProbeConverter.builder(name, livenessProbe).withHttpActionPath(livenessPath.get().getPath()).build()))); } return Optional.empty(); } @@ -997,10 +998,10 @@ private static Optional createReadinessProbe(String name, St Optional readinessPath) { if (readinessProbe.hasUserSuppliedAction()) { return Optional.of(new DecoratorBuildItem(target, - new AddReadinessProbeDecorator(name, ProbeConverter.convert(readinessProbe)))); + new AddReadinessProbeDecorator(name, ProbeConverter.convert(name, readinessProbe)))); } else if (readinessPath.isPresent()) { return Optional.of(new DecoratorBuildItem(target, new AddReadinessProbeDecorator(name, - ProbeConverter.builder(readinessProbe).withHttpActionPath(readinessPath.get().getPath()).build()))); + ProbeConverter.builder(name, readinessProbe).withHttpActionPath(readinessPath.get().getPath()).build()))); } return Optional.empty(); } @@ -1009,10 +1010,10 @@ private static Optional createStartupProbe(String name, Stri Optional startupPath) { if (startupProbe.hasUserSuppliedAction()) { return Optional.of(new DecoratorBuildItem(target, - new AddStartupProbeDecorator(name, ProbeConverter.convert(startupProbe)))); + new AddStartupProbeDecorator(name, ProbeConverter.convert(name, startupProbe)))); } else if (startupPath.isPresent()) { return Optional.of(new DecoratorBuildItem(target, new AddStartupProbeDecorator(name, - ProbeConverter.builder(startupProbe).withHttpActionPath(startupPath.get().getPath()).build()))); + ProbeConverter.builder(name, startupProbe).withHttpActionPath(startupPath.get().getPath()).build()))); } return Optional.empty(); } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConfig.java index b186b82da956c..baeb715e85b65 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConfig.java @@ -54,6 +54,12 @@ public class ProbeConfig { @ConfigItem Optional grpcAction; + /** + * If enabled and `grpc-action` is not provided, it will use the generated service name and the gRPC port. + */ + @ConfigItem(defaultValue = "false") + boolean grpcActionEnabled; + /** * The amount of time to wait before starting to probe. */ diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConverter.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConverter.java index 52b1ad05ca23c..0d76f9f84e340 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConverter.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ProbeConverter.java @@ -1,21 +1,28 @@ package io.quarkus.kubernetes.deployment; +import org.eclipse.microprofile.config.ConfigProvider; + import io.dekorate.kubernetes.config.Probe; import io.dekorate.kubernetes.config.ProbeBuilder; public class ProbeConverter { - public static Probe convert(ProbeConfig probe) { - return builder(probe).build(); + public static Probe convert(String name, ProbeConfig probe) { + return builder(name, probe).build(); } - public static ProbeBuilder builder(ProbeConfig probe) { + public static ProbeBuilder builder(String name, ProbeConfig probe) { ProbeBuilder b = new ProbeBuilder(); probe.httpActionPath.ifPresent(b::withHttpActionPath); probe.execAction.ifPresent(b::withExecAction); probe.tcpSocketAction.ifPresent(b::withTcpSocketAction); - probe.grpcAction.ifPresent(b::withGrpcAction); + if (probe.grpcAction.isPresent()) { + b.withGrpcAction(probe.grpcAction.get()); + } else if (probe.grpcActionEnabled) { + b.withGrpcAction(getQuarkusGrpcPort() + ":" + name); + } + b.withInitialDelaySeconds((int) probe.initialDelay.getSeconds()); b.withPeriodSeconds((int) probe.period.getSeconds()); b.withTimeoutSeconds((int) probe.timeout.getSeconds()); @@ -23,4 +30,9 @@ public static ProbeBuilder builder(ProbeConfig probe) { b.withFailureThreshold(probe.failureThreshold); return b; } + + private static int getQuarkusGrpcPort() { + return ConfigProvider.getConfig().getOptionalValue("quarkus.grpc.server.port", Integer.class) + .orElse(9000); + } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGrpcProbeEnabledTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGrpcProbeEnabledTest.java new file mode 100644 index 0000000000000..05062defc1de0 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGrpcProbeEnabledTest.java @@ -0,0 +1,68 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithGrpcProbeEnabledTest { + + private static final String APP_NAME = "kubernetes-with-grpc-probe"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .overrideConfigKey("quarkus.kubernetes.readiness-probe.grpc-action-enabled", "true") + .setLogFileName("k8s.log") + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getContainers()).singleElement() + .satisfies(container -> { + assertThat(container.getReadinessProbe()).isNotNull().satisfies(p -> { + assertEquals(p.getGrpc().getPort().intValue(), 9000); + assertEquals(p.getGrpc().getService(), APP_NAME); + }); + }); + }); + }); + }); + }); + } +}