From 3b1d5093cf5ef27823cc24ed79cf3979d4f386a5 Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 1 Mar 2022 09:48:28 +0100 Subject: [PATCH] Support gRPC probes for Kubernetes, Openshift and Knative See feature request description in https://github.com/dekorateio/dekorate/issues/857 --- assets/config.md | 4 +- .../dekorate/kubernetes/annotation/Probe.java | 9 +++ .../decorator/AbstractAddProbeDecorator.java | 32 ++++++++- .../decorator/AddLivenessProbeDecorator.java | 1 + .../decorator/AddReadinessProbeDecorator.java | 1 + .../decorator/AddStartupProbeDecorator.java | 1 + .../main/java/io/dekorate/utils/Probes.java | 3 +- docs/configuration-guide.md | 4 +- tests/feat-857-kubernetes-grpc-probes/pom.xml | 58 ++++++++++++++++ .../java/io/dekorate/example/Controller.java | 28 ++++++++ .../main/java/io/dekorate/example/Main.java | 27 ++++++++ .../src/main/resources/application.properties | 5 ++ .../example/GRPCProbesExampleTest.java | 66 +++++++++++++++++++ tests/pom.xml | 1 + 14 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 tests/feat-857-kubernetes-grpc-probes/pom.xml create mode 100644 tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Controller.java create mode 100644 tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Main.java create mode 100644 tests/feat-857-kubernetes-grpc-probes/src/main/resources/application.properties create mode 100644 tests/feat-857-kubernetes-grpc-probes/src/test/java/io/dekorate/example/GRPCProbesExampleTest.java diff --git a/assets/config.md b/assets/config.md index d00349f82..85212c915 100644 --- a/assets/config.md +++ b/assets/config.md @@ -14,7 +14,7 @@ Each option can be: They can be referenced by index inside brackets as in java for example: `dekorate.kubernetes.labels[0]`. ## Complex object -Each property of the complex object can be specified, by expanding the the property key. +Each property of the complex object can be specified, by expanding the property key. For example lets assume the object `Probe` that looks like: | Property | Type | Description | Default Value | @@ -22,6 +22,7 @@ For example lets assume the object `Probe` that looks like: | http-action-path | String | | | | exec-action | String | | | | tcp-socket-action | String | | | +| grpc-action | String | | | | initial-delay-seconds | int | | 0 | | period-seconds | int | | 30 | | timeout-seconds | int | | 10 | @@ -146,6 +147,7 @@ The section below describes all the available subtypes. | http-action-path | String | The http path to use for the probe For this to work, the container port also needs to be set Assuming the container port has been set (as per above comment), if execAction or tcpSocketAction are not set, an http probe will be used automatically even if no path is set (which will result in the root path being used) | | | exec-action | String | The command to use for the probe. | | | tcp-socket-action | String | The tcp socket to use for the probe (the format is host:port). | | +| grpc-action | String | The gRPC port to use for the probe (the format is either port or port:service). | | | initial-delay-seconds | int | The amount of time to wait in seconds before starting to probe. | 0 | | period-seconds | int | The period in which the action should be called. | 30 | | timeout-seconds | int | The amount of time to wait for each action. | 10 | diff --git a/core/src/main/java/io/dekorate/kubernetes/annotation/Probe.java b/core/src/main/java/io/dekorate/kubernetes/annotation/Probe.java index a05960b1a..b2c1e2b15 100644 --- a/core/src/main/java/io/dekorate/kubernetes/annotation/Probe.java +++ b/core/src/main/java/io/dekorate/kubernetes/annotation/Probe.java @@ -45,6 +45,15 @@ */ String tcpSocketAction() default ""; + /** + * The gRPC port to use for the probe (the format is "port"). + * If the health endpoint is configured on a non-default service, you must also specify the service (the format + * is "port:service"). + * + * @return The string representation of the gRPC probe. + */ + String grpcAction() default ""; + /** * The amount of time to wait in seconds before starting to probe. * diff --git a/core/src/main/java/io/dekorate/kubernetes/decorator/AbstractAddProbeDecorator.java b/core/src/main/java/io/dekorate/kubernetes/decorator/AbstractAddProbeDecorator.java index 4b9635eea..37e653910 100644 --- a/core/src/main/java/io/dekorate/kubernetes/decorator/AbstractAddProbeDecorator.java +++ b/core/src/main/java/io/dekorate/kubernetes/decorator/AbstractAddProbeDecorator.java @@ -23,6 +23,7 @@ import io.dekorate.utils.Strings; import io.fabric8.kubernetes.api.model.ContainerFluent; import io.fabric8.kubernetes.api.model.ExecAction; +import io.fabric8.kubernetes.api.model.GRPCAction; import io.fabric8.kubernetes.api.model.HTTPGetAction; import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.TCPSocketAction; @@ -31,7 +32,6 @@ * Base class for any kind of {@link Decorator} that acts on probes. */ public abstract class AbstractAddProbeDecorator extends ApplicationContainerDecorator> { - protected final Probe probe; abstract protected void doCreateProbe(ContainerFluent container, Actions actions); @@ -54,13 +54,14 @@ public void andThenVisit(ContainerFluent container) { final ExecAction execAction = execAction(probe); final TCPSocketAction tcpSocketAction = tcpSocketAction(probe); + final GRPCAction grpcAction = grpcAction(probe); final boolean defaultToHttpGetAction = (execAction == null) && (tcpSocketAction == null); final HTTPGetAction httpGetAction = defaultToHttpGetAction ? httpGetAction(probe, container) : null; if (defaultToHttpGetAction && (httpGetAction == null)) { return; } - doCreateProbe(container, new Actions(execAction, tcpSocketAction, httpGetAction)); + doCreateProbe(container, new Actions(execAction, tcpSocketAction, httpGetAction, grpcAction)); } private ExecAction execAction(Probe probe) { @@ -93,15 +94,40 @@ private TCPSocketAction tcpSocketAction(Probe probe) { return new TCPSocketAction(parts[0], new IntOrString(parts[1])); } + private GRPCAction grpcAction(Probe probe) { + String grpcActionExpression = probe.getGrpcAction(); + if (Strings.isNullOrEmpty(grpcActionExpression)) { + return null; + } + + try { + GRPCAction grpcAction; + if (grpcActionExpression.contains(":")) { + // both port and service is provided + String[] parts = grpcActionExpression.split(":"); + grpcAction = new GRPCAction(Integer.valueOf(parts[0]), parts[1]); + } else { + grpcAction = new GRPCAction(Integer.valueOf(grpcActionExpression), null); + } + + return grpcAction; + } catch (NumberFormatException ex) { + throw new RuntimeException("Wrong port format set in the gRPC probe. Got: " + grpcActionExpression, ex); + } + } + protected static class Actions { protected final ExecAction execAction; protected final TCPSocketAction tcpSocketAction; protected final HTTPGetAction httpGetAction; + protected final GRPCAction grpcAction; - protected Actions(ExecAction execAction, TCPSocketAction tcpSocketAction, HTTPGetAction httpGetAction) { + protected Actions(ExecAction execAction, TCPSocketAction tcpSocketAction, + HTTPGetAction httpGetAction, GRPCAction grpcAction) { this.execAction = execAction; this.tcpSocketAction = tcpSocketAction; this.httpGetAction = httpGetAction; + this.grpcAction = grpcAction; } } } diff --git a/core/src/main/java/io/dekorate/kubernetes/decorator/AddLivenessProbeDecorator.java b/core/src/main/java/io/dekorate/kubernetes/decorator/AddLivenessProbeDecorator.java index 3108004d5..1fea23300 100644 --- a/core/src/main/java/io/dekorate/kubernetes/decorator/AddLivenessProbeDecorator.java +++ b/core/src/main/java/io/dekorate/kubernetes/decorator/AddLivenessProbeDecorator.java @@ -44,6 +44,7 @@ protected void doCreateProbe(ContainerFluent container, Actions actions) { .withExec(actions.execAction) .withHttpGet(actions.httpGetAction) .withTcpSocket(actions.tcpSocketAction) + .withGrpc(actions.grpcAction) .withInitialDelaySeconds(probe.getInitialDelaySeconds()) .withPeriodSeconds(probe.getPeriodSeconds()) .withTimeoutSeconds(probe.getTimeoutSeconds()) diff --git a/core/src/main/java/io/dekorate/kubernetes/decorator/AddReadinessProbeDecorator.java b/core/src/main/java/io/dekorate/kubernetes/decorator/AddReadinessProbeDecorator.java index e1441370e..f53f239fe 100644 --- a/core/src/main/java/io/dekorate/kubernetes/decorator/AddReadinessProbeDecorator.java +++ b/core/src/main/java/io/dekorate/kubernetes/decorator/AddReadinessProbeDecorator.java @@ -36,6 +36,7 @@ protected void doCreateProbe(ContainerFluent container, Actions actions) { .withExec(actions.execAction) .withHttpGet(actions.httpGetAction) .withTcpSocket(actions.tcpSocketAction) + .withGrpc(actions.grpcAction) .withInitialDelaySeconds(probe.getInitialDelaySeconds()) .withPeriodSeconds(probe.getPeriodSeconds()) .withTimeoutSeconds(probe.getTimeoutSeconds()) diff --git a/core/src/main/java/io/dekorate/kubernetes/decorator/AddStartupProbeDecorator.java b/core/src/main/java/io/dekorate/kubernetes/decorator/AddStartupProbeDecorator.java index c0b125fb8..97dc4f738 100644 --- a/core/src/main/java/io/dekorate/kubernetes/decorator/AddStartupProbeDecorator.java +++ b/core/src/main/java/io/dekorate/kubernetes/decorator/AddStartupProbeDecorator.java @@ -36,6 +36,7 @@ protected void doCreateProbe(ContainerFluent container, Actions actions) { .withExec(actions.execAction) .withHttpGet(actions.httpGetAction) .withTcpSocket(actions.tcpSocketAction) + .withGrpc(actions.grpcAction) .withInitialDelaySeconds(probe.getInitialDelaySeconds()) .withPeriodSeconds(probe.getPeriodSeconds()) .withTimeoutSeconds(probe.getTimeoutSeconds()) diff --git a/core/src/main/java/io/dekorate/utils/Probes.java b/core/src/main/java/io/dekorate/utils/Probes.java index 09045f0ee..ce0b86b62 100644 --- a/core/src/main/java/io/dekorate/utils/Probes.java +++ b/core/src/main/java/io/dekorate/utils/Probes.java @@ -28,6 +28,7 @@ public static boolean isConfigured(Probe probe) { return probe != null && (Strings.isNotNullOrEmpty(probe.getHttpActionPath()) || Strings.isNotNullOrEmpty(probe.getExecAction()) - || Strings.isNotNullOrEmpty(probe.getTcpSocketAction())); + || Strings.isNotNullOrEmpty(probe.getTcpSocketAction()) + || Strings.isNotNullOrEmpty(probe.getGrpcAction())); } } diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md index 25224c0f0..d9d12856c 100644 --- a/docs/configuration-guide.md +++ b/docs/configuration-guide.md @@ -23,7 +23,7 @@ Each option can be: They can be referenced by index inside brackets as in java for example: `dekorate.kubernetes.labels[0]`. ## Complex object -Each property of the complex object can be specified, by expanding the the property key. +Each property of the complex object can be specified, by expanding the property key. For example lets assume the object `Probe` that looks like: | Property | Type | Description | Default Value | @@ -31,6 +31,7 @@ For example lets assume the object `Probe` that looks like: | http-action-path | String | | | | exec-action | String | | | | tcp-socket-action | String | | | +| grpc-action | String | | | | initial-delay-seconds | int | | 0 | | period-seconds | int | | 30 | | timeout-seconds | int | | 10 | @@ -169,6 +170,7 @@ The section below describes all the available subtypes. | http-action-path | String | The http path to use for the probe For this to work, the container port also needs to be set Assuming the container port has been set (as per above comment), if execAction or tcpSocketAction are not set, an http probe will be used automatically even if no path is set (which will result in the root path being used) | | | exec-action | String | The command to use for the probe. | | | tcp-socket-action | String | The tcp socket to use for the probe (the format is host:port). | | +| grpc-action | String | The gRPC port to use for the probe (the format is either port or port:service). | | | initial-delay-seconds | int | The amount of time to wait in seconds before starting to probe. | 0 | | period-seconds | int | The period in which the action should be called. | 30 | | timeout-seconds | int | The amount of time to wait for each action. | 10 | diff --git a/tests/feat-857-kubernetes-grpc-probes/pom.xml b/tests/feat-857-kubernetes-grpc-probes/pom.xml new file mode 100644 index 000000000..995f82bce --- /dev/null +++ b/tests/feat-857-kubernetes-grpc-probes/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + + dekorate-tests + io.dekorate + 2.9-SNAPSHOT + ../ + + + io.dekorate + 2.9-SNAPSHOT + feat-857-kubernetes-grpc-probes + Dekorate :: Tests :: gRPC Probes :: Kubernetes + + + + io.dekorate + kubernetes-annotations + ${project.version} + + + io.dekorate + dekorate-spring-boot + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + ${version.spring-boot} + + + + + io.dekorate + kubernetes-junit + ${project.version} + test + + + diff --git a/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Controller.java b/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Controller.java new file mode 100644 index 000000000..ca675cb4a --- /dev/null +++ b/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Controller.java @@ -0,0 +1,28 @@ +/** + * Copyright 2018 The original 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. + */ +package io.dekorate.example; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Controller { + + @RequestMapping("/") + public String hello() { + return "Hello world"; + } +} diff --git a/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Main.java b/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Main.java new file mode 100644 index 000000000..c77b7960e --- /dev/null +++ b/tests/feat-857-kubernetes-grpc-probes/src/main/java/io/dekorate/example/Main.java @@ -0,0 +1,27 @@ +/** + * Copyright 2018 The original 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. + */ +package io.dekorate.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/tests/feat-857-kubernetes-grpc-probes/src/main/resources/application.properties b/tests/feat-857-kubernetes-grpc-probes/src/main/resources/application.properties new file mode 100644 index 000000000..a9b6e845e --- /dev/null +++ b/tests/feat-857-kubernetes-grpc-probes/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# Cover simple port mapping +dekorate.kubernetes.readinessProbe.grpcAction=8000 +# Cover port-service mapping +dekorate.kubernetes.livenessProbe.grpcAction=8001:service +dekorate.kubernetes.startupProbe.grpcAction=8002 diff --git a/tests/feat-857-kubernetes-grpc-probes/src/test/java/io/dekorate/example/GRPCProbesExampleTest.java b/tests/feat-857-kubernetes-grpc-probes/src/test/java/io/dekorate/example/GRPCProbesExampleTest.java new file mode 100644 index 000000000..c6c3c4a63 --- /dev/null +++ b/tests/feat-857-kubernetes-grpc-probes/src/test/java/io/dekorate/example/GRPCProbesExampleTest.java @@ -0,0 +1,66 @@ +/** + * Copyright 2018 The original 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. + */ + +package io.dekorate.example; + +import static io.dekorate.testing.KubernetesResources.findFirst; +import static io.dekorate.testing.KubernetesResources.loadGenerated; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import io.dekorate.utils.Strings; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.Probe; +import io.fabric8.kubernetes.api.model.apps.Deployment; + +class GRPCProbesExampleTest { + + @Test + public void shouldContainProbes() { + KubernetesList list = loadGenerated("kubernetes"); + Optional deployment = findFirst(list, Deployment.class); + assertTrue(deployment.isPresent(), "Deployment not found!"); + + assertReadinessProbe(deployment.get(), 8000, null); + assertLivenessProbe(deployment.get(), 8001, "service"); + assertStartupProbe(deployment.get(), 8002, null); + } + + private static void assertReadinessProbe(Deployment deployment, int port, String service) { + assertProbe(deployment, Container::getReadinessProbe, port, service); + } + + private static void assertLivenessProbe(Deployment deployment, int port, String service) { + assertProbe(deployment, Container::getLivenessProbe, port, service); + } + + private static void assertStartupProbe(Deployment deployment, int port, String service) { + assertProbe(deployment, Container::getStartupProbe, port, service); + } + + private static void assertProbe(Deployment deployment, Function probeFunction, int port, String service) { + + assertTrue(deployment.getSpec().getTemplate().getSpec().getContainers().stream() + .map(probeFunction) + .anyMatch(probe -> Strings.equals(service, probe.getGrpc().getService()) + && port == probe.getGrpc().getPort())); + } +} diff --git a/tests/pom.xml b/tests/pom.xml index 4db0de568..7df09c4c1 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -63,6 +63,7 @@ feat-kubernetes-probes feat-openshift-probes feat-knative-probes + feat-857-kubernetes-grpc-probes