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/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