From b6cacc44e1a204ec24c63c58192f01a92f536ece Mon Sep 17 00:00:00 2001 From: ttugrul <13340482+ttugrul@users.noreply.github.com> Date: Wed, 24 Dec 2025 19:31:44 +0300 Subject: [PATCH] feat: add configurable port binding address for dockerized services --- .../docker/config/DockerConfiguration.java | 18 +++++- .../docker/impl/DockerizedService.java | 56 +++++++++++++++++-- .../docker/impl/DockerizedServicesModule.java | 1 + 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/modules/dockerized-services/api/src/main/java/eu/cloudnetservice/modules/docker/config/DockerConfiguration.java b/modules/dockerized-services/api/src/main/java/eu/cloudnetservice/modules/docker/config/DockerConfiguration.java index 7eff9ffbc6..cea502a624 100644 --- a/modules/dockerized-services/api/src/main/java/eu/cloudnetservice/modules/docker/config/DockerConfiguration.java +++ b/modules/dockerized-services/api/src/main/java/eu/cloudnetservice/modules/docker/config/DockerConfiguration.java @@ -38,9 +38,12 @@ public record DockerConfiguration( @Nullable String registryPassword, @Nullable String registryUrl, @Nullable String user, - @Nullable HostAndPort nodeHostOverride + @Nullable HostAndPort nodeHostOverride, + @Nullable String portBindAddress ) { + public static final String AUTO_PORT_BIND_ADDRESS = "auto"; + public static @NonNull Builder builder() { return new Builder(); } @@ -59,7 +62,9 @@ public record DockerConfiguration( .registryEmail(configuration.registryEmail()) .registryPassword(configuration.registryPassword()) .registryUrl(configuration.registryUrl()) - .user(configuration.user()); + .user(configuration.user()) + .nodeHostOverride(configuration.nodeHostOverride()) + .portBindAddress(configuration.portBindAddress()); } public static final class Builder { @@ -82,6 +87,7 @@ public static final class Builder { private String user; private HostAndPort nodeHostOverride; + private String portBindAddress; public @NonNull Builder javaImage(@NonNull DockerImage javaImage) { this.javaImage = javaImage; @@ -168,6 +174,11 @@ public static final class Builder { return this; } + public @NonNull Builder portBindAddress(@Nullable String portBindAddress) { + this.portBindAddress = portBindAddress; + return this; + } + public @NonNull DockerConfiguration build() { Preconditions.checkNotNull(this.javaImage, "Java docker image must be given"); return new DockerConfiguration( @@ -184,7 +195,8 @@ public static final class Builder { this.registryPassword, this.registryUrl, this.user, - this.nodeHostOverride); + this.nodeHostOverride, + this.portBindAddress); } } } diff --git a/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedService.java b/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedService.java index d4ada22d14..89a2fa0395 100644 --- a/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedService.java +++ b/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedService.java @@ -29,6 +29,7 @@ import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.InternetProtocol; import com.github.dockerjava.api.model.LogConfig; +import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.RestartPolicy; import com.github.dockerjava.api.model.Volume; import eu.cloudnetservice.driver.document.property.DocProperty; @@ -54,6 +55,8 @@ import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collection; @@ -229,6 +232,36 @@ protected void doStartProcess( // an isolated, single java installation available which is always accessible via 'java' arguments.set(0, "java"); + // build host configuration + var hostConfig = HostConfig.newHostConfig() + .withBinds(binds) + .withCapDrop(DROPPED_CAPABILITIES) + .withRestartPolicy(RestartPolicy.noRestart()) + .withNetworkMode(this.configuration.network()) + .withLogConfig(new LogConfig(LogConfig.LoggingType.LOCAL, LOGGING_OPTIONS)); + + // add port bindings if configured and not using host network mode + var portBindAddress = this.configuration.portBindAddress(); + if (portBindAddress != null && !"host".equals(this.configuration.network())) { + var servicePort = this.serviceConfiguration.port(); + + // resolve the bind address if it's set to "auto" (uses hostAddress) + String resolvedBindAddress; + if (DockerConfiguration.AUTO_PORT_BIND_ADDRESS.equals(portBindAddress)) { + resolvedBindAddress = this.resolveHostAddress(this.serviceConfiguration.hostAddress()); + } else { + resolvedBindAddress = portBindAddress; + } + + // only add port bindings if we have a valid bind address + if (resolvedBindAddress != null) { + var portBindings = new Ports(); + portBindings.bind(ExposedPort.tcp(servicePort), Ports.Binding.bindIpAndPort(resolvedBindAddress, servicePort)); + portBindings.bind(ExposedPort.udp(servicePort), Ports.Binding.bindIpAndPort(resolvedBindAddress, servicePort)); + hostConfig.withPortBindings(portBindings); + } + } + // create the container and store the container id this.containerId = this.dockerClient.createContainerCmd(image.imageName()) .withEnv(env) @@ -243,12 +276,7 @@ protected void doStartProcess( .withExposedPorts(exposedPorts) .withName(this.serviceId().name() + "_" + this.serviceId().uniqueId()) .withWorkingDir(this.serviceDirectory.toAbsolutePath().toString()) - .withHostConfig(HostConfig.newHostConfig() - .withBinds(binds) - .withCapDrop(DROPPED_CAPABILITIES) - .withRestartPolicy(RestartPolicy.noRestart()) - .withNetworkMode(this.configuration.network()) - .withLogConfig(new LogConfig(LogConfig.LoggingType.LOCAL, LOGGING_OPTIONS))) + .withHostConfig(hostConfig) .withLabels(Map.of( "Service", "CloudNet", "Name", this.serviceId().name(), @@ -402,6 +430,22 @@ protected boolean needsImagePull(@NonNull DockerImage image) { return new Bind(path, new Volume(path), accessMode); } + protected @Nullable String resolveHostAddress(@NonNull String hostAddress) { + try { + var inetAddress = InetAddress.getByName(hostAddress); + var resolvedAddress = inetAddress.getHostAddress(); + // remove IPv6 scope identifier if present (e.g., fe80::1%eth0 -> fe80::1) + var scopeIndex = resolvedAddress.indexOf('%'); + if (scopeIndex != -1) { + resolvedAddress = resolvedAddress.substring(0, scopeIndex); + } + return resolvedAddress; + } catch (UnknownHostException exception) { + LOGGER.warn("Unable to resolve host address {} for port binding", hostAddress, exception); + return null; + } + } + public final class ServiceLogCacheAdapter extends ResultCallback.Adapter { @Override diff --git a/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedServicesModule.java b/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedServicesModule.java index 45ee2f49ec..492d2930ba 100644 --- a/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedServicesModule.java +++ b/modules/dockerized-services/impl/src/main/java/eu/cloudnetservice/modules/docker/impl/DockerizedServicesModule.java @@ -59,6 +59,7 @@ public void loadConfiguration() { null, null, null, + null, null), DocumentFactory.json()); }