diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 2161f2e7b24a82..83b50a5c231ab5 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -712,6 +712,10 @@ The table below describe all the available configuration options. | quarkus.kubernetes.resources.requests.memory | String | | | quarkus.kubernetes.resources.limits.cpu | String | | | quarkus.kubernetes.resources.limits.memory | String | | +| quarkus.kubernetes.remote-debug.enabled | boolean | | false +| quarkus.kubernetes.remote-debug.transport | String | | dt_socket +| quarkus.kubernetes.remote-debug.address-port | int | | 5005 +| quarkus.kubernetes.remote-debug.suspend | String | | n |==== Properties that use non-standard types, can be referenced by expanding the property. @@ -994,6 +998,10 @@ The OpenShift resources can be customized in a similar approach with Kubernetes. | quarkus.openshift.route.host | String | | | quarkus.openshift.route.annotations | Map | | | quarkus.openshift.headless | boolean | | false +| quarkus.openshift.remote-debug.enabled | boolean | | false +| quarkus.openshift.remote-debug.transport | String | | dt_socket +| quarkus.openshift.remote-debug.address-port | int | | 5005 +| quarkus.openshift.remote-debug.suspend | String | | n |==== [#knative] @@ -1214,6 +1222,21 @@ In other words the extension will use whatever cluster `kubectl` uses. The same At the moment no additional options are provided for further customization. +=== Remote Debugging + +To remotely debug applications that are running on a kubernetes environment, we need to deploy the application as described in the previous section and add as new property: `quarkus.kubernetes.remote-debug.enabled=true`. This property will automatically configure the Java application to append the java agent configuration (for example: `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005`) and also the service resource to listen using the java agent port. + +After your application has been deployed with the debug enabled, next you need to tunnel the traffic from your local host machine to the specified port of the java agent: + +[source,bash,subs=attributes+] +---- +kubectl port-forward svc/ 5005:5005 +---- + +Using this command, you'll forward the traffic from the "localhost:5005" to the kubernetes service running the java agent using the port "5005" which is the one that the java agent uses by default for remote debugging. You can also configure another java agent port using the property `quarkus.kubernetes.remote-debug.address-port`. + +Finally, all you need to do is to configure your favorite IDE to attach the java agent process that is forwarded to `localhost:5005` and start to debug your application. For example, in IntelliJ IDEA, you can follow https://www.jetbrains.com/help/idea/tutorial-remote-debug.html:[this tutorial] to debug remote applications. + == Using existing resources Sometimes it's desirable to either provide additional resources (e.g. a ConfigMap, a Secret, a Deployment for a database etc) or provide custom ones that will be used as a `base` for the generation process. diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DebugConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DebugConfig.java new file mode 100644 index 00000000000000..f7a497503c4f0c --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DebugConfig.java @@ -0,0 +1,55 @@ +package io.quarkus.kubernetes.deployment; + +import io.dekorate.kubernetes.config.Env; +import io.dekorate.kubernetes.config.EnvBuilder; +import io.dekorate.kubernetes.config.Port; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class DebugConfig { + + private static final String PORT_NAME = "debug"; + private static final String JAVA_TOOL_OPTIONS = "JAVA_TOOL_OPTIONS"; + private static final String AGENTLIB_FORMAT = "-agentlib:jdwp=transport=%s,server=y,suspend=%s,address=%s"; + + /** + * If true, the debug mode in pods will be enabled. + */ + @ConfigItem(defaultValue = "false") + boolean enabled; + + /** + * The transport to use. + */ + @ConfigItem(defaultValue = "dt_socket") + String transport; + + /** + * If enabled, it means the JVM will wait for the debugger to attach before executing the main class. + * If false, the JVM will immediately execute the main class, while listening for + * the debugger connection. + */ + @ConfigItem(defaultValue = "n") + String suspend; + + /** + * It specifies the address at which the debug socket will listen. + */ + @ConfigItem(defaultValue = "5005") + Integer addressPort; + + protected Env buildJavaToolOptionsEnv() { + return new EnvBuilder() + .withName(JAVA_TOOL_OPTIONS) + .withValue(String.format(AGENTLIB_FORMAT, transport, suspend, addressPort)) + .build(); + } + + protected Port buildDebugPort() { + return Port.newBuilder() + .withName(PORT_NAME) + .withContainerPort(addressPort) + .build(); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java index c1b7283b538379..f8b33bb4f8c796 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java @@ -292,6 +292,11 @@ public enum DeploymentResourceKind { @ConfigItem Optional appConfigMap; + /** + * Debug configuration to be set in pods. + */ + DebugConfig remoteDebug; + public Optional getPartOf() { return partOf; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java index f332f7e022b32b..89a4ebbd684e44 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java @@ -497,6 +497,11 @@ public EnvVarsConfig getEnv() { @ConfigItem Optional appConfigMap; + /** + * Debug configuration to be set in pods. + */ + DebugConfig remoteDebug; + public Optional getAppSecret() { return this.appSecret; } @@ -511,9 +516,9 @@ public Optional getExposition() { } public static boolean isOpenshiftBuildEnabled(ContainerImageConfig containerImageConfig, Capabilities capabilities) { - boolean implictlyEnabled = ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities) + boolean implicitlyEnabled = ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities) .filter(c -> c.contains(OPENSHIFT) || c.contains(S2I)).isPresent(); - return containerImageConfig.builder.map(b -> b.equals(OPENSHIFT) || b.equals(S2I)).orElse(implictlyEnabled); + return containerImageConfig.builder.map(b -> b.equals(OPENSHIFT) || b.equals(S2I)).orElse(implicitlyEnabled); } public DeploymentResourceKind getDeploymentResourceKind() { diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index c179feaa875166..07847be91ab566 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -133,6 +133,11 @@ public List createConfigurators(ApplicationInfoBuildItem }); result.add(new ConfiguratorBuildItem(new ApplyExpositionConfigurator(config.route))); + // Handle remote debug configuration for container ports + if (config.remoteDebug.enabled) { + result.add(new ConfiguratorBuildItem(new AddPortToOpenshiftConfig(config.remoteDebug.buildDebugPort()))); + } + if (!capabilities.isPresent(Capability.CONTAINER_IMAGE_S2I) && !capabilities.isPresent("io.quarkus.openshift") && !capabilities.isPresent(Capability.CONTAINER_IMAGE_OPENSHIFT)) { @@ -277,6 +282,13 @@ public List createDecorators(ApplicationInfoBuildItem applic new AddDockerImageStreamResourceDecorator(imageConfiguration, repositoryWithRegistry))); }); } + + // Handle remote debug configuration + if (config.remoteDebug.enabled) { + result.add(new DecoratorBuildItem(OPENSHIFT, new AddEnvVarDecorator(ApplicationContainerDecorator.ANY, name, + config.remoteDebug.buildJavaToolOptionsEnv()))); + } + return result; } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 09c4dc55278002..344ba2551a90b1 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -102,6 +102,12 @@ public List createConfigurators(KubernetesConfig config, result.add(new ConfiguratorBuildItem(new AddPortToKubernetesConfig(e.getValue()))); }); result.add(new ConfiguratorBuildItem(new ApplyExpositionConfigurator((config.ingress)))); + + // Handle remote debug configuration for container ports + if (config.remoteDebug.enabled) { + result.add(new ConfiguratorBuildItem(new AddPortToKubernetesConfig(config.remoteDebug.buildDebugPort()))); + } + return result; } @@ -181,6 +187,12 @@ public List createDecorators(ApplicationInfoBuildItem applic .findFirst().orElse(DEFAULT_HTTP_PORT); result.add(new DecoratorBuildItem(KUBERNETES, new ApplyHttpGetActionPortDecorator(name, name, port))); + // Handle remote debug configuration + if (config.remoteDebug.enabled) { + result.add(new DecoratorBuildItem(KUBERNETES, new AddEnvVarDecorator(ApplicationContainerDecorator.ANY, name, + config.remoteDebug.buildJavaToolOptionsEnv()))); + } + return result; } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRemoteDebugTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRemoteDebugTest.java new file mode 100644 index 00000000000000..6024d3fe964d99 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRemoteDebugTest.java @@ -0,0 +1,65 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +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.EnvVar; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithRemoteDebugTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("kubernetes-with-debug") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-with-debug.properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + List openshiftList = DeserializationUtil.deserializeAsList( + kubernetesDir.resolve("kubernetes.yml")); + + assertThat(openshiftList).filteredOn(h -> "Deployment".equals(h.getKind())).singleElement().satisfies(h -> { + Deployment deployment = (Deployment) h; + assertThat(deployment.getSpec().getTemplate().getSpec().getContainers()).singleElement().satisfies(container -> { + List envVars = container.getEnv(); + assertThat(envVars).anySatisfy(envVar -> { + assertThat(envVar.getName()).isEqualTo("JAVA_TOOL_OPTIONS"); + assertThat(envVar.getValue()) + .isEqualTo("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000"); + }); + }); + }); + + assertThat(openshiftList).filteredOn(h -> "Service".equals(h.getKind())).singleElement().satisfies(h -> { + Service service = (Service) h; + assertThat(service.getSpec().getPorts()).anySatisfy(p -> { + assertThat(p.getName()).isEqualTo("http"); + assertThat(p.getPort()).isEqualTo(80); + assertThat(p.getTargetPort().getIntVal()).isEqualTo(8080); + }); + + assertThat(service.getSpec().getPorts()).anySatisfy(p -> { + assertThat(p.getName()).isEqualTo("debug"); + assertThat(p.getPort()).isEqualTo(8000); + assertThat(p.getTargetPort().getIntVal()).isEqualTo(8000); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithRemoteDebugTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithRemoteDebugTest.java new file mode 100644 index 00000000000000..83e7a8a7b2d60b --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithRemoteDebugTest.java @@ -0,0 +1,74 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.openshift.api.model.DeploymentConfig; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithRemoteDebugTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("openshift-with-debug") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("openshift-with-debug.properties") + .setForcedDependencies(Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-openshift", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List openshiftList = DeserializationUtil.deserializeAsList( + kubernetesDir.resolve("openshift.yml")); + + assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> { + DeploymentConfig deployment = (DeploymentConfig) h; + assertThat(deployment.getSpec().getTemplate().getSpec().getContainers()).singleElement().satisfies(container -> { + List envVars = container.getEnv(); + assertThat(envVars).anySatisfy(envVar -> { + assertThat(envVar.getName()).isEqualTo("JAVA_TOOL_OPTIONS"); + assertThat(envVar.getValue()) + .isEqualTo("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"); + }); + }); + }); + + assertThat(openshiftList).filteredOn(h -> "Service".equals(h.getKind())).singleElement().satisfies(h -> { + Service service = (Service) h; + assertThat(service.getSpec().getPorts()).anySatisfy(p -> { + assertThat(p.getName()).isEqualTo("http"); + assertThat(p.getPort()).isEqualTo(80); + assertThat(p.getTargetPort().getIntVal()).isEqualTo(8080); + }); + + assertThat(service.getSpec().getPorts()).anySatisfy(p -> { + assertThat(p.getName()).isEqualTo("debug"); + assertThat(p.getPort()).isEqualTo(5005); + assertThat(p.getTargetPort().getIntVal()).isEqualTo(5005); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-debug.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-debug.properties new file mode 100644 index 00000000000000..de4ed4ba87ec6c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-debug.properties @@ -0,0 +1,2 @@ +quarkus.kubernetes.remote-debug.enabled=true +quarkus.kubernetes.remote-debug.address-port=8000 diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-debug.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-debug.properties new file mode 100644 index 00000000000000..384a22bebb76f9 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-debug.properties @@ -0,0 +1 @@ +quarkus.openshift.remote-debug.enabled=true