From d661bf83b2705f1aa52aed5ff7fada00a8dc9b99 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis <geoand@gmail.com> Date: Thu, 3 Jun 2021 11:49:31 +0300 Subject: [PATCH] Allow specifying per-port nodePort Resolves: #17582 --- .../deployment/MinikubeProcessor.java | 15 +++- .../deployment/AddNodePortDecorator.java | 29 ++++++- .../kubernetes/deployment/PortConfig.java | 14 +++- .../VanillaKubernetesProcessor.java | 14 +++- .../KubernetesWithMultiplePortsTest.java | 81 +++++++++++++++++++ .../kubernetes-with-multiple-ports.properties | 9 +++ 6 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMultiplePortsTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-multiple-ports.properties diff --git a/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java b/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java index 9c0bba4c82e577..46e69b97345663 100644 --- a/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java +++ b/extensions/kubernetes/minikube/deployment/src/main/java/io/quarkus/minikube/deployment/MinikubeProcessor.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.dekorate.kubernetes.annotation.ServiceType; @@ -42,6 +43,7 @@ import io.quarkus.kubernetes.deployment.EnvConverter; import io.quarkus.kubernetes.deployment.KubernetesCommonHelper; import io.quarkus.kubernetes.deployment.KubernetesConfig; +import io.quarkus.kubernetes.deployment.PortConfig; import io.quarkus.kubernetes.deployment.ResourceNameUtil; import io.quarkus.kubernetes.spi.ConfiguratorBuildItem; import io.quarkus.kubernetes.spi.DecoratorBuildItem; @@ -139,8 +141,17 @@ public List<DecoratorBuildItem> createDecorators(ApplicationInfoBuildItem applic //Service handling result.add(new DecoratorBuildItem(MINIKUBE, new ApplyServiceTypeDecorator(name, ServiceType.NodePort.name()))); - result.add(new DecoratorBuildItem(MINIKUBE, new AddNodePortDecorator(name, config.getNodePort() - .orElseGet(() -> getStablePortNumberInRange(name, MIN_NODE_PORT_VALUE, MAX_NODE_PORT_VALUE))))); + List<PortConfig> nodeConfigPorts = config.getPorts().values().stream().filter(pc -> pc.nodePort.isPresent()) + .collect(Collectors.toList()); + if (!nodeConfigPorts.isEmpty()) { + for (PortConfig portConfig : nodeConfigPorts) { + result.add(new DecoratorBuildItem(KUBERNETES, + new AddNodePortDecorator(name, portConfig.nodePort.getAsInt(), portConfig.containerPort))); + } + } else { + result.add(new DecoratorBuildItem(MINIKUBE, new AddNodePortDecorator(name, config.getNodePort() + .orElseGet(() -> getStablePortNumberInRange(name, MIN_NODE_PORT_VALUE, MAX_NODE_PORT_VALUE))))); + } //Probe port handling Integer port = ports.stream().filter(p -> HTTP_PORT.equals(p.getName())).map(KubernetesPortBuildItem::getPort) diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodePortDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodePortDecorator.java index 045b7419a0bdba..39b51a73b79686 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodePortDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNodePortDecorator.java @@ -2,11 +2,15 @@ import static io.quarkus.kubernetes.deployment.Constants.*; +import java.util.OptionalInt; +import java.util.function.Predicate; + import org.jboss.logging.Logger; import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.kubernetes.decorator.NamedResourceDecorator; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; import io.fabric8.kubernetes.api.model.ServiceSpecFluent; public class AddNodePortDecorator extends NamedResourceDecorator<ServiceSpecFluent> { @@ -14,21 +18,40 @@ public class AddNodePortDecorator extends NamedResourceDecorator<ServiceSpecFlue private static final Logger log = Logger.getLogger(AddNodePortDecorator.class); private final int nodePort; + private final OptionalInt matchingTargetPort; public AddNodePortDecorator(String name, int nodePort) { + this(name, nodePort, OptionalInt.empty()); + } + + public AddNodePortDecorator(String name, int nodePort, OptionalInt matchingTargetPort) { super(name); if (nodePort < MIN_NODE_PORT_VALUE || nodePort > MAX_NODE_PORT_VALUE) { log.info("Using a port outside of the " + MIN_NODE_PORT_VALUE + "-" + MAX_NODE_PORT_VALUE + " range might not work, see https://kubernetes.io/docs/concepts/services-networking/service/#nodeport"); } this.nodePort = nodePort; + this.matchingTargetPort = matchingTargetPort; } + @SuppressWarnings("unchecked") @Override public void andThenVisit(ServiceSpecFluent service, ObjectMeta resourceMeta) { - ServiceSpecFluent.PortsNested<?> editFirstPort = service.editFirstPort(); - editFirstPort.withNodePort(nodePort); - editFirstPort.endPort(); + ServiceSpecFluent.PortsNested<?> editPort; + if (matchingTargetPort.isPresent()) { + editPort = service.editMatchingPort(new Predicate<ServicePortBuilder>() { + @Override + public boolean test(ServicePortBuilder servicePortBuilder) { + return (servicePortBuilder.buildTargetPort() != null) + && (servicePortBuilder.buildTargetPort().getIntVal() != null) + && (matchingTargetPort.getAsInt() == servicePortBuilder.buildTargetPort().getIntVal()); + } + }); + } else { + editPort = service.editFirstPort(); + } + editPort.withNodePort(nodePort); + editPort.endPort(); } @Override diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PortConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PortConfig.java index 67eda9a8f4372f..0a431297b3b792 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PortConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PortConfig.java @@ -15,13 +15,13 @@ public class PortConfig { * The port number. Refers to the container port. */ @ConfigItem - OptionalInt containerPort; + public OptionalInt containerPort; /** * The host port. */ @ConfigItem - OptionalInt hostPort; + public OptionalInt hostPort; /** * The application path (refers to web application path). @@ -29,12 +29,18 @@ public class PortConfig { * @return The path, defaults to /. */ @ConfigItem(defaultValue = "/") - Optional<String> path; + public Optional<String> path; /** * The protocol. */ @ConfigItem(defaultValue = "TCP") - Protocol protocol; + public Protocol protocol; + + /** + * The nodePort to which this port should be mapped to. + * This only takes affect when the serviceType is set to node-port. + */ + public OptionalInt nodePort; } 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 beae8e595eb5ce..75fa7635afaec6 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 @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.dekorate.kubernetes.annotation.ServiceType; @@ -133,8 +134,17 @@ public List<DecoratorBuildItem> createDecorators(ApplicationInfoBuildItem applic // Service handling result.add(new DecoratorBuildItem(KUBERNETES, new ApplyServiceTypeDecorator(name, config.getServiceType().name()))); - if ((config.getServiceType() == ServiceType.NodePort) && config.nodePort.isPresent()) { - result.add(new DecoratorBuildItem(KUBERNETES, new AddNodePortDecorator(name, config.nodePort.getAsInt()))); + if ((config.getServiceType() == ServiceType.NodePort)) { + List<PortConfig> nodeConfigPorts = config.ports.values().stream().filter(pc -> pc.nodePort.isPresent()) + .collect(Collectors.toList()); + if (!nodeConfigPorts.isEmpty()) { + for (PortConfig portConfig : nodeConfigPorts) { + result.add(new DecoratorBuildItem(KUBERNETES, + new AddNodePortDecorator(name, portConfig.nodePort.getAsInt(), portConfig.containerPort))); + } + } else if (config.nodePort.isPresent()) { + result.add(new DecoratorBuildItem(KUBERNETES, new AddNodePortDecorator(name, config.nodePort.getAsInt()))); + } } // Probe port handling diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMultiplePortsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMultiplePortsTest.java new file mode 100644 index 00000000000000..0e784fb82ef527 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMultiplePortsTest.java @@ -0,0 +1,81 @@ +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.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +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.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 KubernetesWithMultiplePortsTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("kubernetes-with-multiple-ports") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-with-multiple-ports.properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List<HasMetadata> kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + assertThat(kubernetesList).hasSize(2); + + assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("kubernetes-with-multiple-ports"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + assertThat(container.getPorts()).hasSize(2); + assertThat(container.getPorts()).filteredOn(cp -> cp.getContainerPort() == 8080).hasSize(1); + assertThat(container.getPorts()).filteredOn(cp -> cp.getContainerPort() == 5005).hasSize(1); + }); + }); + }); + }); + }); + }); + + assertThat(kubernetesList).filteredOn(i -> "Service".equals(i.getKind())).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Service.class, s -> { + assertThat(s.getSpec()).satisfies(spec -> { + assertEquals("NodePort", spec.getType()); + assertThat(spec.getPorts()).hasSize(2); + assertThat(spec.getPorts()).filteredOn(sp -> sp.getPort() == 8080).singleElement().satisfies(p -> { + assertThat(p.getNodePort()).isEqualTo(30000); + }); + assertThat(spec.getPorts()).filteredOn(sp -> sp.getPort() == 5005).singleElement().satisfies(p -> { + assertThat(p.getNodePort()).isEqualTo(31000); + }); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-multiple-ports.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-multiple-ports.properties new file mode 100644 index 00000000000000..e3ef96683ea626 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-multiple-ports.properties @@ -0,0 +1,9 @@ +quarkus.kubernetes.ports.http.name=http +quarkus.kubernetes.ports.http.host-port=8080 +quarkus.kubernetes.ports.http.container-port=8080 +quarkus.kubernetes.ports.http.node-port=30000 +quarkus.kubernetes.ports.remote.name=http +quarkus.kubernetes.ports.remote.host-port=5005 +quarkus.kubernetes.ports.remote.container-port=5005 +quarkus.kubernetes.ports.remote.node-port=31000 +quarkus.kubernetes.service-type=NodePort