From 354a579788773974c99be0e4022a1bdaf9b895fa Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:01:42 +0000 Subject: [PATCH 01/41] add-traefik-support: Added a stack config setting to allow the reverse-proxy to be specified. --- .../stack/clients/core/StackClient.java | 9 +++++++++ .../cmclinnovations/stack/services/ServiceManager.java | 10 ++++++---- .../src/main/java/com/cmclinnovations/stack/Stack.java | 1 + .../java/com/cmclinnovations/stack/StackConfig.java | 8 ++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java index 8350f876..2c3d3c95 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java @@ -36,6 +36,7 @@ public final class StackClient { private static StackHost stackHost = new StackHost(); private static boolean isolated = false; + private static String reverseProxyName; static { String envVarStackName = System.getenv(StackClient.STACK_NAME_KEY); @@ -107,6 +108,14 @@ public static Path getAbsDataPath() { return getStackBaseDir().resolve("inputs").resolve("data"); } + public static void setReverseProxyName(String reverseProxyName) { + StackClient.reverseProxyName = reverseProxyName; + } + + public static String getReverseProxyName() { + return reverseProxyName; + } + /** * Get a RemoteRDBStoreClient for the named Postgres RDB running in this stack. * diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index 14a92f7e..d87d2dc4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -167,15 +167,17 @@ public S initialiseService(String stackName, String serviceN DockerService dockerService = getOrInitialiseService(stackName, StackClient.getContainerEngineName()); dockerService.doPreStartUpConfiguration(newContainerService); + if (!StackClient.getReverseProxyName().equals(serviceName)) { + ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, + StackClient.getReverseProxyName()); + reverseProxyService.addService(newContainerService); + } + dockerService.writeEndpointConfigs(newContainerService); if (dockerService.startContainer(newContainerService)) { dockerService.doFirstTimePostStartUpConfiguration(newContainerService); } dockerService.doEveryTimePostStartUpConfiguration(newContainerService); - if (!NginxService.TYPE.equals(serviceName)) { - ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, NginxService.TYPE); - reverseProxyService.addService(newContainerService); - } } services.put(serviceName, newService); diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 692cdc6e..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -83,6 +83,7 @@ private Stack(String name, ServiceManager manager, StackConfig config) { if (null != config) { StackClient.setStackHost(config.getHost()); StackClient.setIsolated(config.isIsolated()); + StackClient.setReverseProxyName(config.getReverseProxyName()); } } diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 95b2d62a..7c4470ec 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -7,6 +7,7 @@ import java.util.Map; import com.cmclinnovations.stack.clients.core.StackHost; +import com.cmclinnovations.stack.services.NginxService; import com.fasterxml.jackson.annotation.JsonProperty; public class StackConfig { @@ -30,6 +31,9 @@ private enum Selector { @JsonProperty private final Boolean isolated = false; + @JsonProperty("reverseProxy") + private final String reverseProxy = NginxService.TYPE; + @JsonProperty("hostName") private void setHostName(String hostName) { host = new StackHost(hostName); @@ -54,4 +58,8 @@ Map getVolumes() { public boolean isIsolated() { return isolated; } + + public String getReverseProxyName() { + return reverseProxy; + } } From 4f645cbf6e7f771c953e9483b89197cd66c3d1fb Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:03:13 +0000 Subject: [PATCH 02/41] add-traefik-support: Bumped stack version. --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 1596f5b1..5801c070 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index a76fd8d7..c0b5e052 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2492d196..c110fea3 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 9a621abf..4f6025ef 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 83e67e62..f2536823 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index ab2fc7ba..d8984e8a 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT From 1124705c8ddb1aecb75fc62c754f22ee50ea9c0d Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:07:39 +0000 Subject: [PATCH 03/41] add-traefik-support: Added "empty" `TraefikService` class. --- .../stack/services/TraefikService.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java new file mode 100644 index 00000000..090be337 --- /dev/null +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -0,0 +1,36 @@ +package com.cmclinnovations.stack.services; + +import java.util.HashMap; +import java.util.Map; + +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; +import com.cmclinnovations.stack.services.config.Connection; +import com.cmclinnovations.stack.services.config.ServiceConfig; + +public class TraefikService extends ContainerService implements ReverseProxyService { + + public TraefikService(String stackName, ServiceConfig config) { + super(stackName, config); + } + + @Override + public void addService(ContainerService service) { + Map labels = service.getContainerSpec().getLabels(); + if (null == labels) { + labels = new HashMap<>(); + service.getContainerSpec().withLabels(labels); + } + labels.put("traefik.enable", "true"); + + service.getConfig().getEndpoints().forEach((name,connection) -> { + // TODO: Set the labels correctly + // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // labels.put( + // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); + }); + } + +} From 84a1fd5101908c65144ce3f924cbbcb211d9945e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 11:12:55 +0000 Subject: [PATCH 04/41] get debugger working --- stack-manager/.vscode/tasks.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stack-manager/.vscode/tasks.json b/stack-manager/.vscode/tasks.json index af41b2f3..06455fe2 100644 --- a/stack-manager/.vscode/tasks.json +++ b/stack-manager/.vscode/tasks.json @@ -33,7 +33,10 @@ ], "options": { "shell": { - "executable": "bash" + "executable": "bash", + "args": [ + "-c" + ] } } }, From 52332ce27ebf8665f4441009379e7777f0bf7b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:27 +0000 Subject: [PATCH 05/41] traefik service config file --- .../stack/services/built-ins/traefik.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json new file mode 100644 index 00000000..ff525b4c --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -0,0 +1,46 @@ +{ + "type": "traefik", + "ServiceSpec": { + "Name": "traefik", + "TaskTemplate": { + "ContainerSpec": { + "Image": "traefik:v3.6", + "Mounts": [ + { + "Type": "bind", + "Source": "/var/run/docker.sock", + "Target": "/var/run/docker.sock", + "ReadOnly": true + }, + { + "Type": "volume", + "Source": "traefik_config", + "Target": "/etc/traefik" + } + ] + } + }, + "EndpointSpec": { + "Ports": [ + { + "Name": "web", + "Protocol": "tcp", + "TargetPort": "80", + "PublishedPort": "3838" + }, + { + "Name": "websecure", + "Protocol": "tcp", + "TargetPort": "443", + "PublishedPort": "443" + }, + { + "Name": "dashboard", + "Protocol": "tcp", + "TargetPort": "8080", + "PublishedPort": "8080" + } + ] + } + } +} \ No newline at end of file From 461b7c684f5520a262d558fd660cd1ba2bbb3da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:47 +0000 Subject: [PATCH 06/41] addreverseproxy method in stack.java --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 14136c07..159b769d 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,6 +135,9 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } + // Add the reverse proxy service specified in the config + addReverseProxyIfAbsent(selectedServices); + // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -142,6 +145,15 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } + private void addReverseProxyIfAbsent(List selectedServices) { + String reverseProxyService = config.getReverseProxyName(); + if (reverseProxyService != null && !reverseProxyService.isEmpty()) { + if (!selectedServices.contains(reverseProxyService)) { + selectedServices.add(reverseProxyService); + } + } + } + private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 88aca33e37413b9120be16319ae4a3860b53a666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 11:44:24 +0000 Subject: [PATCH 07/41] remove snashot --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 4 ++-- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 1596f5b1..42751b47 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index a76fd8d7..8bd6ce18 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Clients https://theworldavatar.io @@ -15,7 +15,7 @@ uk.ac.cam.cares.jps jps-parent-pom - 2.3.2 + 2.4.0 diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2492d196..2ab1be15 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 9a621abf..e22659f2 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 83e67e62..609e3b0a 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index ab2fc7ba..0ed7134d 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT From 711978e7841690b0bbe3058cc81e4308bb9542de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 12:19:09 +0000 Subject: [PATCH 08/41] fix version --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 42751b47..2f32c919 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3 secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index 8bd6ce18..4d14832d 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2ab1be15..c08d56a0 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3 secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index e22659f2..04986269 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 609e3b0a..95afaba4 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3 environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index 0ed7134d..bd991a9b 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 From 53e725ca0e2307fa8b97ae0441c3df8da9d91e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:02:11 +0000 Subject: [PATCH 09/41] rm redundant import --- .../com/cmclinnovations/stack/clients/core/datasets/RML.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java index a9ca70f6..cb6deb73 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java @@ -1,7 +1,6 @@ package com.cmclinnovations.stack.clients.core.datasets; import java.nio.file.Path; -import java.util.Map; import com.cmclinnovations.stack.clients.rml.RmlMapperClient; From 7667dd97fae47f907d9cb683e14cae2f4c28fac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:07:25 +0000 Subject: [PATCH 10/41] replace deprecated docker api call --- .../stack/clients/docker/DockerClient.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java index 85391b99..a8b5cde4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java @@ -53,7 +53,9 @@ import com.github.dockerjava.core.DefaultDockerClientConfig.Builder; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.StreamType; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; @@ -115,7 +117,6 @@ public String executeSimpleCommand(String containerId, String... cmd) { .withOutputStream(outputStream) .withErrorStream(outputStream) .exec(); - String output = outputStream.toString(); return execId; } @@ -231,11 +232,21 @@ public String exec() { execStartCmd.withStdIn(inputStream); } - // ExecStartResultCallback is marked deprecated but seems to do exactly what we - // want and without knowing why it is deprecated any issues with it can't be - // overcome anyway. - try (ExecStartResultCallback result = execStartCmd - .exec(new ExecStartResultCallback(outputStream, errorStream))) { + try (ResultCallback.Adapter result = execStartCmd + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + try { + if (frame.getStreamType() == StreamType.STDOUT && outputStream != null) { + outputStream.write(frame.getPayload()); + } else if (frame.getStreamType() == StreamType.STDERR && errorStream != null) { + errorStream.write(frame.getPayload()); + } + } catch (IOException ex) { + throw new RuntimeException("Failed to write frame payload", ex); + } + } + })) { if (wait) { if (!result.awaitCompletion(evaluationTimeout, TimeUnit.SECONDS)) { LOGGER.warn("Docker exec command '{}' still running after the {} second execution timeout.", @@ -553,7 +564,7 @@ public boolean isContainerUp(String containerName) { public String getContainerId(String containerName) { return getContainer(containerName).map(Container::getId) - .orElseThrow(() -> new NoSuchElementException("Cannot get container "+containerName+".")); + .orElseThrow(() -> new NoSuchElementException("Cannot get container " + containerName + ".")); } private Map> convertToConfigFilterMap(String configName, Map labelMap) { From dc8dee177ac240dbad1b5838b1eabeb64fcd1a00 Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:01:42 +0000 Subject: [PATCH 11/41] add-traefik-support: Added a stack config setting to allow the reverse-proxy to be specified. --- .../stack/clients/core/StackClient.java | 9 +++++++++ .../cmclinnovations/stack/services/ServiceManager.java | 10 ++++++---- .../src/main/java/com/cmclinnovations/stack/Stack.java | 1 + .../java/com/cmclinnovations/stack/StackConfig.java | 8 ++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java index 8350f876..2c3d3c95 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java @@ -36,6 +36,7 @@ public final class StackClient { private static StackHost stackHost = new StackHost(); private static boolean isolated = false; + private static String reverseProxyName; static { String envVarStackName = System.getenv(StackClient.STACK_NAME_KEY); @@ -107,6 +108,14 @@ public static Path getAbsDataPath() { return getStackBaseDir().resolve("inputs").resolve("data"); } + public static void setReverseProxyName(String reverseProxyName) { + StackClient.reverseProxyName = reverseProxyName; + } + + public static String getReverseProxyName() { + return reverseProxyName; + } + /** * Get a RemoteRDBStoreClient for the named Postgres RDB running in this stack. * diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index 14a92f7e..d87d2dc4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -167,15 +167,17 @@ public S initialiseService(String stackName, String serviceN DockerService dockerService = getOrInitialiseService(stackName, StackClient.getContainerEngineName()); dockerService.doPreStartUpConfiguration(newContainerService); + if (!StackClient.getReverseProxyName().equals(serviceName)) { + ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, + StackClient.getReverseProxyName()); + reverseProxyService.addService(newContainerService); + } + dockerService.writeEndpointConfigs(newContainerService); if (dockerService.startContainer(newContainerService)) { dockerService.doFirstTimePostStartUpConfiguration(newContainerService); } dockerService.doEveryTimePostStartUpConfiguration(newContainerService); - if (!NginxService.TYPE.equals(serviceName)) { - ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, NginxService.TYPE); - reverseProxyService.addService(newContainerService); - } } services.put(serviceName, newService); diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 692cdc6e..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -83,6 +83,7 @@ private Stack(String name, ServiceManager manager, StackConfig config) { if (null != config) { StackClient.setStackHost(config.getHost()); StackClient.setIsolated(config.isIsolated()); + StackClient.setReverseProxyName(config.getReverseProxyName()); } } diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 95b2d62a..7c4470ec 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -7,6 +7,7 @@ import java.util.Map; import com.cmclinnovations.stack.clients.core.StackHost; +import com.cmclinnovations.stack.services.NginxService; import com.fasterxml.jackson.annotation.JsonProperty; public class StackConfig { @@ -30,6 +31,9 @@ private enum Selector { @JsonProperty private final Boolean isolated = false; + @JsonProperty("reverseProxy") + private final String reverseProxy = NginxService.TYPE; + @JsonProperty("hostName") private void setHostName(String hostName) { host = new StackHost(hostName); @@ -54,4 +58,8 @@ Map getVolumes() { public boolean isIsolated() { return isolated; } + + public String getReverseProxyName() { + return reverseProxy; + } } From d04d90d5fb3cdb19dc4407a255faf89cd620dfce Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:03:13 +0000 Subject: [PATCH 12/41] add-traefik-support: Bumped stack version. --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 2f32c919..5801c070 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index 4d14832d..b1d14acb 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index c08d56a0..c110fea3 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 04986269..4f6025ef 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 95afaba4..f2536823 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index bd991a9b..d8984e8a 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT From 70871f6b95e079b07d92f97b6289d02393334050 Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:07:39 +0000 Subject: [PATCH 13/41] add-traefik-support: Added "empty" `TraefikService` class. --- .../stack/services/TraefikService.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java new file mode 100644 index 00000000..090be337 --- /dev/null +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -0,0 +1,36 @@ +package com.cmclinnovations.stack.services; + +import java.util.HashMap; +import java.util.Map; + +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; +import com.cmclinnovations.stack.services.config.Connection; +import com.cmclinnovations.stack.services.config.ServiceConfig; + +public class TraefikService extends ContainerService implements ReverseProxyService { + + public TraefikService(String stackName, ServiceConfig config) { + super(stackName, config); + } + + @Override + public void addService(ContainerService service) { + Map labels = service.getContainerSpec().getLabels(); + if (null == labels) { + labels = new HashMap<>(); + service.getContainerSpec().withLabels(labels); + } + labels.put("traefik.enable", "true"); + + service.getConfig().getEndpoints().forEach((name,connection) -> { + // TODO: Set the labels correctly + // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // labels.put( + // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); + }); + } + +} From e882d97a1086b96c4acf92ed611c5bdf19d261d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 11:12:55 +0000 Subject: [PATCH 14/41] get debugger working --- stack-manager/.vscode/tasks.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stack-manager/.vscode/tasks.json b/stack-manager/.vscode/tasks.json index af41b2f3..06455fe2 100644 --- a/stack-manager/.vscode/tasks.json +++ b/stack-manager/.vscode/tasks.json @@ -33,7 +33,10 @@ ], "options": { "shell": { - "executable": "bash" + "executable": "bash", + "args": [ + "-c" + ] } } }, From 14bb232e7b3fc5eb782ba26ec75a20594f8bcc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:27 +0000 Subject: [PATCH 15/41] traefik service config file --- .../stack/services/built-ins/traefik.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json new file mode 100644 index 00000000..ff525b4c --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -0,0 +1,46 @@ +{ + "type": "traefik", + "ServiceSpec": { + "Name": "traefik", + "TaskTemplate": { + "ContainerSpec": { + "Image": "traefik:v3.6", + "Mounts": [ + { + "Type": "bind", + "Source": "/var/run/docker.sock", + "Target": "/var/run/docker.sock", + "ReadOnly": true + }, + { + "Type": "volume", + "Source": "traefik_config", + "Target": "/etc/traefik" + } + ] + } + }, + "EndpointSpec": { + "Ports": [ + { + "Name": "web", + "Protocol": "tcp", + "TargetPort": "80", + "PublishedPort": "3838" + }, + { + "Name": "websecure", + "Protocol": "tcp", + "TargetPort": "443", + "PublishedPort": "443" + }, + { + "Name": "dashboard", + "Protocol": "tcp", + "TargetPort": "8080", + "PublishedPort": "8080" + } + ] + } + } +} \ No newline at end of file From f0d7239908bfc7744f220440a5fbc2ad8faf7914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:47 +0000 Subject: [PATCH 16/41] addreverseproxy method in stack.java --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 14136c07..159b769d 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,6 +135,9 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } + // Add the reverse proxy service specified in the config + addReverseProxyIfAbsent(selectedServices); + // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -142,6 +145,15 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } + private void addReverseProxyIfAbsent(List selectedServices) { + String reverseProxyService = config.getReverseProxyName(); + if (reverseProxyService != null && !reverseProxyService.isEmpty()) { + if (!selectedServices.contains(reverseProxyService)) { + selectedServices.add(reverseProxyService); + } + } + } + private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 8bc930532b44569921cdf280a36c64db9690c909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:02:11 +0000 Subject: [PATCH 17/41] rm redundant import --- .../com/cmclinnovations/stack/clients/core/datasets/RML.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java index a9ca70f6..cb6deb73 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java @@ -1,7 +1,6 @@ package com.cmclinnovations.stack.clients.core.datasets; import java.nio.file.Path; -import java.util.Map; import com.cmclinnovations.stack.clients.rml.RmlMapperClient; From e81af7a1eb429e1193d04147d78fc9ef97cb07b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:07:25 +0000 Subject: [PATCH 18/41] replace deprecated docker api call --- .../stack/clients/docker/DockerClient.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java index 85391b99..a8b5cde4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java @@ -53,7 +53,9 @@ import com.github.dockerjava.core.DefaultDockerClientConfig.Builder; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.StreamType; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; @@ -115,7 +117,6 @@ public String executeSimpleCommand(String containerId, String... cmd) { .withOutputStream(outputStream) .withErrorStream(outputStream) .exec(); - String output = outputStream.toString(); return execId; } @@ -231,11 +232,21 @@ public String exec() { execStartCmd.withStdIn(inputStream); } - // ExecStartResultCallback is marked deprecated but seems to do exactly what we - // want and without knowing why it is deprecated any issues with it can't be - // overcome anyway. - try (ExecStartResultCallback result = execStartCmd - .exec(new ExecStartResultCallback(outputStream, errorStream))) { + try (ResultCallback.Adapter result = execStartCmd + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + try { + if (frame.getStreamType() == StreamType.STDOUT && outputStream != null) { + outputStream.write(frame.getPayload()); + } else if (frame.getStreamType() == StreamType.STDERR && errorStream != null) { + errorStream.write(frame.getPayload()); + } + } catch (IOException ex) { + throw new RuntimeException("Failed to write frame payload", ex); + } + } + })) { if (wait) { if (!result.awaitCompletion(evaluationTimeout, TimeUnit.SECONDS)) { LOGGER.warn("Docker exec command '{}' still running after the {} second execution timeout.", @@ -553,7 +564,7 @@ public boolean isContainerUp(String containerName) { public String getContainerId(String containerName) { return getContainer(containerName).map(Container::getId) - .orElseThrow(() -> new NoSuchElementException("Cannot get container "+containerName+".")); + .orElseThrow(() -> new NoSuchElementException("Cannot get container " + containerName + ".")); } private Map> convertToConfigFilterMap(String configName, Map labelMap) { From 26f6c7dbfb21b2b930319beab7b5d2fa55ed52a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:32:31 +0000 Subject: [PATCH 19/41] rm duplicate docker socket mount for traefik --- .../cmclinnovations/stack/services/built-ins/traefik.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json index ff525b4c..31e9f55d 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -6,12 +6,6 @@ "ContainerSpec": { "Image": "traefik:v3.6", "Mounts": [ - { - "Type": "bind", - "Source": "/var/run/docker.sock", - "Target": "/var/run/docker.sock", - "ReadOnly": true - }, { "Type": "volume", "Source": "traefik_config", From 5784de33c26bb68fef45be285fe32fbae7cdbe99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:32:50 +0000 Subject: [PATCH 20/41] give traefik service clasee a type field --- .../stack/services/TraefikService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 090be337..d8218f69 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -3,13 +3,12 @@ import java.util.HashMap; import java.util.Map; -import com.cmclinnovations.stack.clients.core.StackClient; -import com.cmclinnovations.stack.clients.utils.FileUtils; -import com.cmclinnovations.stack.services.config.Connection; import com.cmclinnovations.stack.services.config.ServiceConfig; public class TraefikService extends ContainerService implements ReverseProxyService { + public static final String TYPE = "traefik"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); } @@ -23,13 +22,15 @@ public void addService(ContainerService service) { } labels.put("traefik.enable", "true"); - service.getConfig().getEndpoints().forEach((name,connection) -> { + service.getConfig().getEndpoints().forEach((name, connection) -> { // TODO: Set the labels correctly // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", - // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), + // true, false) + "`)"); // labels.put( - // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", - // String.valueOf(connection.getInternalPort())); + // "traefik.http.services." + service.getServiceName() + + // ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); }); } From 1b4617e32bf7b3b5f7e0bf5106bc74f7ca72d184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:39:34 +0000 Subject: [PATCH 21/41] rm bad traefik service starter method --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 159b769d..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,9 +135,6 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } - // Add the reverse proxy service specified in the config - addReverseProxyIfAbsent(selectedServices); - // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -145,15 +142,6 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } - private void addReverseProxyIfAbsent(List selectedServices) { - String reverseProxyService = config.getReverseProxyName(); - if (reverseProxyService != null && !reverseProxyService.isEmpty()) { - if (!selectedServices.contains(reverseProxyService)) { - selectedServices.add(reverseProxyService); - } - } - } - private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 6b50203d2002c5909156f220815843b6da6f596c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:40:09 +0000 Subject: [PATCH 22/41] fix the reverseProxy string field in StackConfig --- .../src/main/java/com/cmclinnovations/stack/StackConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 7c4470ec..fc97f92a 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import com.cmclinnovations.stack.clients.core.StackHost; import com.cmclinnovations.stack.services.NginxService; @@ -32,7 +33,7 @@ private enum Selector { private final Boolean isolated = false; @JsonProperty("reverseProxy") - private final String reverseProxy = NginxService.TYPE; + private final Optional reverseProxy = Optional.empty(); @JsonProperty("hostName") private void setHostName(String hostName) { @@ -60,6 +61,6 @@ public boolean isIsolated() { } public String getReverseProxyName() { - return reverseProxy; + return reverseProxy.orElse(NginxService.TYPE); } } From 55d4d6b4e558cba6964fa93fdd591365957b392e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:42:45 +0000 Subject: [PATCH 23/41] rm an unused import --- .../com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java index d75c0528..6dd94585 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java @@ -4,7 +4,6 @@ import org.eclipse.rdf4j.federated.repository.FedXRepositoryConfigBuilder; import org.eclipse.rdf4j.repository.config.RepositoryConfig; -import org.eclipse.rdf4j.repository.config.RepositoryImplConfig; import org.eclipse.rdf4j.repository.manager.RemoteRepositoryManager; import org.eclipse.rdf4j.repository.sail.config.SailRepositoryConfig; import org.eclipse.rdf4j.repository.sparql.config.SPARQLRepositoryConfig; From 702924e768283d9878387aa2851ed4c7217e03c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:13:40 +0000 Subject: [PATCH 24/41] organise imports --- .../stack/clients/docker/PodmanClient.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java index 3ee82684..c0a87e01 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java @@ -7,15 +7,16 @@ import com.cmclinnovations.stack.clients.core.StackClient; import com.cmclinnovations.stack.clients.utils.JsonHelper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.model.Config; +import com.github.dockerjava.jaxrs.ApiClientExtension; + import io.theworldavatar.swagger.podman.ApiClient; import io.theworldavatar.swagger.podman.ApiException; import io.theworldavatar.swagger.podman.api.NetworksApi; import io.theworldavatar.swagger.podman.api.SecretsApi; import io.theworldavatar.swagger.podman.model.Network; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.dockerjava.api.model.Config; -import com.github.dockerjava.jaxrs.ApiClientExtension; public class PodmanClient extends DockerClient { From fbaa083cef5efbe81aa6b359511e0905c6ab6a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:13:57 +0000 Subject: [PATCH 25/41] add a traefik config file --- .../services/traefik/configs/traefik.yml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml new file mode 100644 index 00000000..78e77013 --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -0,0 +1,21 @@ +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + traefik: + address: ":8080" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + swarmMode: true + network: "${STACK_NAME}" + +log: + level: INFO From 9224d02fb6fbc8b6f79afcbf2a89f8aecf0acc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:15:30 +0000 Subject: [PATCH 26/41] implement traefik config method --- .../stack/services/TraefikService.java | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index d8218f69..02c91221 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -1,28 +1,80 @@ package com.cmclinnovations.stack.services; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; import com.cmclinnovations.stack.services.config.ServiceConfig; +import com.github.dockerjava.api.model.ContainerSpec; public class TraefikService extends ContainerService implements ReverseProxyService { public static final String TYPE = "traefik"; + private static final String TRAEFIK_CONF_DIR = "/etc/traefik/"; + private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); } @Override - public void addService(ContainerService service) { - Map labels = service.getContainerSpec().getLabels(); - if (null == labels) { - labels = new HashMap<>(); - service.getContainerSpec().withLabels(labels); + public void doFirstTimePostStartUpConfiguration() { + try (InputStream inStream = new BufferedInputStream( + TraefikService.class.getResourceAsStream(TRAEFIK_CONFIG_TEMPLATE))) { + + String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); + // Replace the ${STACK_NAME} placeholder with actual stack name + String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); + configContent = configContent.replace("${STACK_NAME}", stackName); + + // Write the configuration file to the container + Map files = new HashMap<>(); + files.put("traefik.yml", configContent.getBytes(StandardCharsets.UTF_8)); + sendFilesContent(files, TRAEFIK_CONF_DIR); + + } catch (IOException ex) { + throw new RuntimeException("Failed to configure Traefik", ex); } + } + + @Override + public void addService(ContainerService service) { + ContainerSpec containerSpec = service.getContainerSpec(); + Map labels = new HashMap<>(); + containerSpec.withLabels(labels); labels.put("traefik.enable", "true"); - service.getConfig().getEndpoints().forEach((name, connection) -> { + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { + URI externalPath = connection.getExternalPath(); + if (null != externalPath) { + String serviceName = service.getName(); + String routerName = serviceName + "_" + endpointName; + String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); + + // Configure router with path prefix rule + labels.put("traefik.http.routers." + routerName + ".rule", + "PathPrefix(`" + pathPrefix + "`)"); + labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); + + // Configure service with the internal port + URL url = connection.getUrl(); + int port = url.getPort(); + if (port == -1) { + port = 80; // Default port + } + labels.put("traefik.http.routers." + routerName + ".service", routerName); + labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", + String.valueOf(port)); + } + // TODO: Set the labels correctly // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), From 0c469a57be43b86f0ebab5f3cd532ecd6f43a592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 22 Jan 2026 12:16:36 +0000 Subject: [PATCH 27/41] format and do imports on save for everyone --- stack-clients/.vscode/settings.json | 9 +++++++++ stack-data-uploader/.vscode/settings.json | 9 +++++++++ stack-manager/.vscode/settings.json | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 stack-clients/.vscode/settings.json create mode 100644 stack-data-uploader/.vscode/settings.json create mode 100644 stack-manager/.vscode/settings.json diff --git a/stack-clients/.vscode/settings.json b/stack-clients/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-clients/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} diff --git a/stack-data-uploader/.vscode/settings.json b/stack-data-uploader/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-data-uploader/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} diff --git a/stack-manager/.vscode/settings.json b/stack-manager/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-manager/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} From a95a58a44f99f38bf3b7782404488db07b8f4817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 22 Jan 2026 12:17:03 +0000 Subject: [PATCH 28/41] fix the provider in traefik.yaml --- .../stack/services/traefik/configs/traefik.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 78e77013..8c0a18a7 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://json.schemastore.org/traefik-v3.json api: dashboard: true insecure: true @@ -11,10 +12,9 @@ entryPoints: address: ":8080" providers: - docker: + swarm: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false - swarmMode: true network: "${STACK_NAME}" log: From d817ff95454da78f90fa390ead5130d56e3b9272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:49:49 +0000 Subject: [PATCH 29/41] merge the labels --- .../stack/services/DockerService.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java index b79ceb22..669a1bc8 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java @@ -9,6 +9,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -338,8 +339,14 @@ void removeService(String serviceName) { protected ServiceSpec configureServiceSpec(ContainerService service) { ServiceSpec serviceSpec = service.getServiceSpec() - .withName(service.getContainerName()) - .withLabels(StackClient.getStackNameLabelMap()); + .withName(service.getContainerName()); + // Merge existing labels with stack name labels + Map serviceLabels = new HashMap<>(); + if (serviceSpec.getLabels() != null) { + serviceLabels.putAll(serviceSpec.getLabels()); + } + serviceLabels.putAll(StackClient.getStackNameLabelMap()); + serviceSpec.withLabels(serviceLabels); TaskSpec taskTemplate = service.getTaskTemplate(); if (null == taskTemplate.getRestartPolicy()) { taskTemplate.withRestartPolicy(new ServiceRestartPolicy() @@ -350,8 +357,14 @@ protected ServiceSpec configureServiceSpec(ContainerService service) { .withTarget(network.getId()) .withAliases(List.of(service.getName())))); ContainerSpec containerSpec = service.getContainerSpec() - .withLabels(StackClient.getStackNameLabelMap()) .withHostname(service.getName()); + // Merge existing container labels with stack name labels + Map containerLabels = new HashMap<>(); + if (containerSpec.getLabels() != null) { + containerLabels.putAll(containerSpec.getLabels()); + } + containerLabels.putAll(StackClient.getStackNameLabelMap()); + containerSpec.withLabels(containerLabels); interpolateEnvironmentVariables(containerSpec); From 5640561bfc9f39181e8a8500bb45cfed9c42e736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:29 +0000 Subject: [PATCH 30/41] better name for a method --- .../com/cmclinnovations/stack/services/NginxService.java | 2 +- .../stack/services/ReverseProxyService.java | 2 +- .../com/cmclinnovations/stack/services/ServiceManager.java | 2 +- .../test/java/com/cmclinnovations/stack/StackHostTest.java | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java index f8024fc5..f3058fae 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java @@ -78,7 +78,7 @@ private void updateExternalPort(ServiceConfig config) { } } - public void addService(ContainerService service) { + public void addStackServiceToReverseProxy(ContainerService service) { NgxConfig locationConfigOut = new NgxConfig(); diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java index cc0175f2..b1b927b6 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java @@ -2,5 +2,5 @@ public interface ReverseProxyService extends Service { - public void addService(ContainerService service); + public void addStackServiceToReverseProxy(ContainerService service); } diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index d87d2dc4..7615600e 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -170,7 +170,7 @@ public S initialiseService(String stackName, String serviceN if (!StackClient.getReverseProxyName().equals(serviceName)) { ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, StackClient.getReverseProxyName()); - reverseProxyService.addService(newContainerService); + reverseProxyService.addStackServiceToReverseProxy(newContainerService); } dockerService.writeEndpointConfigs(newContainerService); diff --git a/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java b/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java index 73f62998..d40d97ee 100644 --- a/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java +++ b/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java @@ -45,11 +45,12 @@ void testNameJson() { () -> Assertions.assertEquals("host", stackHost.getStringBuilder().withName().build())); } - @Test - void testEmptyStrings() { + @Test + void testEmptyStrings() { StackHost stackHostDefault = new StackHost(); StackHost stackHostJson = Assertions - .assertDoesNotThrow(() -> objectMapper.readValue("{\"proto\":\"\", \"name\":\"\",\"port\":\" \",\"path\":\" \"}", StackHost.class)); + .assertDoesNotThrow(() -> objectMapper + .readValue("{\"proto\":\"\", \"name\":\"\",\"port\":\" \",\"path\":\" \"}", StackHost.class)); Assertions.assertAll( () -> Assertions.assertEquals(stackHostDefault.getProto(), stackHostJson.getProto()), () -> Assertions.assertEquals(stackHostDefault.getName(), stackHostJson.getName()), From 01fe3679bab8bb7c1b8e89d51e3911441337e073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:42 +0000 Subject: [PATCH 31/41] loads of label stuff --- .../stack/services/TraefikService.java | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 02c91221..8e0d724b 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -6,19 +6,28 @@ import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.docker.DockerClient; import com.cmclinnovations.stack.clients.utils.FileUtils; import com.cmclinnovations.stack.services.config.ServiceConfig; import com.github.dockerjava.api.model.ContainerSpec; +import com.github.dockerjava.api.model.ContainerSpecConfig; +import com.github.dockerjava.api.model.ContainerSpecFile; +import com.github.dockerjava.api.model.ServiceSpec; public class TraefikService extends ContainerService implements ReverseProxyService { + private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; + public static final String TYPE = "traefik"; - private static final String TRAEFIK_CONF_DIR = "/etc/traefik/"; + private static final String TRAEFIK_CONFIG_NAME = "traefik_config"; + private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; public TraefikService(String stackName, ServiceConfig config) { @@ -26,19 +35,40 @@ public TraefikService(String stackName, ServiceConfig config) { } @Override - public void doFirstTimePostStartUpConfiguration() { + protected void doPreStartUpConfiguration() { try (InputStream inStream = new BufferedInputStream( TraefikService.class.getResourceAsStream(TRAEFIK_CONFIG_TEMPLATE))) { String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); // Replace the ${STACK_NAME} placeholder with actual stack name String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); + String stackPortString = System.getenv(EXTERNAL_PORT); + configContent = configContent.replace("${STACK_NAME}", stackName); + configContent = configContent.replace("${STACK_PORT}", stackPortString); - // Write the configuration file to the container - Map files = new HashMap<>(); - files.put("traefik.yml", configContent.getBytes(StandardCharsets.UTF_8)); - sendFilesContent(files, TRAEFIK_CONF_DIR); + // Create Docker Config for Traefik + DockerClient dockerClient = DockerClient.getInstance(); + if (!dockerClient.configExists(TRAEFIK_CONFIG_NAME)) { + dockerClient.addConfig(TRAEFIK_CONFIG_NAME, configContent.getBytes(StandardCharsets.UTF_8)); + } + + // Mount the config into the container + ContainerSpec containerSpec = getContainerSpec(); + List configs = containerSpec.getConfigs(); + if (null == configs) { + configs = new ArrayList<>(); + containerSpec.withConfigs(configs); + } + + ContainerSpecConfig traefikConfig = new ContainerSpecConfig() + .withConfigName(TRAEFIK_CONFIG_NAME) + .withFile(new ContainerSpecFile() + .withName(TRAEFIK_CONFIG_PATH) + .withUid("0") + .withGid("0") + .withMode(0444L)); + configs.add(traefikConfig); } catch (IOException ex) { throw new RuntimeException("Failed to configure Traefik", ex); @@ -46,16 +76,17 @@ public void doFirstTimePostStartUpConfiguration() { } @Override - public void addService(ContainerService service) { - ContainerSpec containerSpec = service.getContainerSpec(); - Map labels = new HashMap<>(); - containerSpec.withLabels(labels); + public void addStackServiceToReverseProxy(ContainerService service) { + // Traefik's Swarm provider reads service-level labels, not container labels + ServiceSpec serviceSpec = service.getServiceSpec(); + Map existingLabels = serviceSpec.getLabels(); + final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); labels.put("traefik.enable", "true"); service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { - String serviceName = service.getName(); + String serviceName = service.getContainerName(); String routerName = serviceName + "_" + endpointName; String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); @@ -84,6 +115,9 @@ public void addService(ContainerService service) { // ".loadbalancer.server.port", // String.valueOf(connection.getInternalPort())); }); + + // Set labels on the service spec after they've been populated + serviceSpec.withLabels(labels); } } From 8e92c93fb40c2ab3d0cd96bc8ffa70216d70e02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:53 +0000 Subject: [PATCH 32/41] customisable stack port --- .../cmclinnovations/stack/services/traefik/configs/traefik.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 8c0a18a7..7e7a5e0f 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -5,7 +5,7 @@ api: entryPoints: web: - address: ":80" + address: ":${STACK_PORT}" websecure: address: ":443" traefik: From 72e070292b5339b6819b321c71f4f63b22c7eb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 19:13:51 +0000 Subject: [PATCH 33/41] rm the bad host port casting --- .../java/com/cmclinnovations/stack/services/TraefikService.java | 2 -- .../cmclinnovations/stack/services/traefik/configs/traefik.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 8e0d724b..1abf0d30 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -42,10 +42,8 @@ protected void doPreStartUpConfiguration() { String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); // Replace the ${STACK_NAME} placeholder with actual stack name String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); - String stackPortString = System.getenv(EXTERNAL_PORT); configContent = configContent.replace("${STACK_NAME}", stackName); - configContent = configContent.replace("${STACK_PORT}", stackPortString); // Create Docker Config for Traefik DockerClient dockerClient = DockerClient.getInstance(); diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 7e7a5e0f..8c0a18a7 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -5,7 +5,7 @@ api: entryPoints: web: - address: ":${STACK_PORT}" + address: ":80" websecure: address: ":443" traefik: From 26af8d87915c479ed6bbaedfb1c76123c0f0cb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 28 Jan 2026 17:12:25 +0000 Subject: [PATCH 34/41] reproduce specific port functionality --- .../stack/services/NginxService.java | 23 +--------- .../stack/services/ReverseProxyService.java | 43 +++++++++++++++++++ .../stack/services/TraefikService.java | 10 +---- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java index f3058fae..cbdd2e62 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java @@ -14,8 +14,6 @@ import com.cmclinnovations.stack.exceptions.InvalidTemplateException; import com.cmclinnovations.stack.services.config.Connection; import com.cmclinnovations.stack.services.config.ServiceConfig; -import com.github.dockerjava.api.model.EndpointSpec; -import com.github.dockerjava.api.model.PortConfig; import com.github.odiszapc.nginxparser.NgxBlock; import com.github.odiszapc.nginxparser.NgxComment; import com.github.odiszapc.nginxparser.NgxConfig; @@ -27,8 +25,6 @@ public final class NginxService extends ContainerService implements ReverseProxyService { - private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; - public static final String TYPE = "nginx"; private static final String TEMPLATE_TYPE = "Nginx config"; @@ -63,21 +59,6 @@ protected void doPreStartUpConfiguration() { } } - private void updateExternalPort(ServiceConfig config) { - String externalPort = System.getenv(EXTERNAL_PORT); - if (null != externalPort) { - EndpointSpec endpointSpec = config.getDockerServiceSpec().getEndpointSpec(); - if (null != endpointSpec) { - List ports = endpointSpec.getPorts(); - if (null != ports) { - ports.stream() - .filter(port -> port.getTargetPort() == 80) - .forEach(port -> port.withPublishedPort(Integer.parseInt(externalPort))); - } - } - } - } - public void addStackServiceToReverseProxy(ContainerService service) { NgxConfig locationConfigOut = new NgxConfig(); @@ -170,9 +151,7 @@ private String getProxyPassValue(Connection connection, String hostname) { } private String getServerURL(Connection connection, String hostname) { - URL url = connection.getUrl(); - int port = url.getPort(); - return hostname + ":" + ((-1 == port) ? 80 : port); + return hostname + ":" + getPortOrDefault(connection.getUrl()); } private final class ConfigSender { diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java index b1b927b6..7bcc6503 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java @@ -1,6 +1,49 @@ package com.cmclinnovations.stack.services; +import java.net.URL; +import java.util.List; + +import com.cmclinnovations.stack.services.config.ServiceConfig; +import com.github.dockerjava.api.model.EndpointSpec; +import com.github.dockerjava.api.model.PortConfig; + public interface ReverseProxyService extends Service { public void addStackServiceToReverseProxy(ContainerService service); + + /** + * Updates the external port mapping for the reverse proxy service. + * This allows multiple stacks to run on the same host by exposing each stack's + * reverse proxy on a different external port. + * + * @param config The service configuration containing the endpoint + * specifications + */ + default void updateExternalPort(ServiceConfig config) { + String externalPort = System.getenv("EXTERNAL_PORT"); + if (null != externalPort) { + EndpointSpec endpointSpec = config.getDockerServiceSpec().getEndpointSpec(); + if (null != endpointSpec) { + List ports = endpointSpec.getPorts(); + if (null != ports) { + ports.stream() + .filter(port -> port.getTargetPort() == 80) + .forEach(port -> port.withPublishedPort(Integer.parseInt(externalPort))); + } + } + } + } + + /** + * Gets the port from a URL, defaulting to 80 if not specified. + * This is a common pattern when working with HTTP services that don't + * explicitly specify a port. + * + * @param url The URL to extract the port from + * @return The port number, or 80 if the URL doesn't specify a port (-1) + */ + default int getPortOrDefault(URL url) { + int port = url.getPort(); + return (port == -1) ? 80 : port; + } } diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 1abf0d30..bd263750 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -22,8 +21,6 @@ public class TraefikService extends ContainerService implements ReverseProxyService { - private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; - public static final String TYPE = "traefik"; private static final String TRAEFIK_CONFIG_NAME = "traefik_config"; @@ -32,6 +29,7 @@ public class TraefikService extends ContainerService implements ReverseProxyServ public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); + updateExternalPort(config); } @Override @@ -94,11 +92,7 @@ public void addStackServiceToReverseProxy(ContainerService service) { labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); // Configure service with the internal port - URL url = connection.getUrl(); - int port = url.getPort(); - if (port == -1) { - port = 80; // Default port - } + int port = getPortOrDefault(connection.getUrl()); labels.put("traefik.http.routers." + routerName + ".service", routerName); labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", String.valueOf(port)); From 2755320e16f2575b299a9105082a824509f8ea46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 28 Jan 2026 18:54:55 +0000 Subject: [PATCH 35/41] try an auth setup with keycloak env vars --- .../stack/services/TraefikService.java | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index bd263750..9bc63dc3 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -27,6 +27,12 @@ public class TraefikService extends ContainerService implements ReverseProxyServ private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; + // Keycloak authentication configuration + private static final String KEYCLOAK_AUTH_ENABLED = "KEYCLOAK_AUTH_ENABLED"; + private static final String KEYCLOAK_AUTH_URL = "KEYCLOAK_AUTH_URL"; + private static final String KEYCLOAK_REALM = "KEYCLOAK_REALM"; + private static final String AUTH_MIDDLEWARE_NAME = "keycloak-auth"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); updateExternalPort(config); @@ -79,6 +85,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); labels.put("traefik.enable", "true"); + // Check if Keycloak authentication is enabled globally + boolean authEnabled = isKeycloakAuthEnabled(); + String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { @@ -91,25 +101,66 @@ public void addStackServiceToReverseProxy(ContainerService service) { "PathPrefix(`" + pathPrefix + "`)"); labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); + // Add authentication middleware if enabled + if (authMiddleware != null) { + labels.put("traefik.http.routers." + routerName + ".middlewares", authMiddleware); + } + // Configure service with the internal port int port = getPortOrDefault(connection.getUrl()); labels.put("traefik.http.routers." + routerName + ".service", routerName); labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", String.valueOf(port)); } - - // TODO: Set the labels correctly - // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", - // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), - // true, false) + "`)"); - // labels.put( - // "traefik.http.services." + service.getServiceName() + - // ".loadbalancer.server.port", - // String.valueOf(connection.getInternalPort())); }); + // If auth is enabled, configure the ForwardAuth middleware globally for this + // Traefik instance + if (authEnabled) { + configureKeycloakAuthMiddleware(labels); + } + // Set labels on the service spec after they've been populated serviceSpec.withLabels(labels); } + /** + * Checks if Keycloak authentication is enabled via environment variable. + */ + private boolean isKeycloakAuthEnabled() { + String enabled = System.getenv(KEYCLOAK_AUTH_ENABLED); + return "true".equalsIgnoreCase(enabled); + } + + /** + * Configures the Keycloak ForwardAuth middleware on the Traefik service. + * This middleware will be applied to all routers that reference it. + */ + private void configureKeycloakAuthMiddleware(Map labels) { + String authUrl = System.getenv(KEYCLOAK_AUTH_URL); + String realm = System.getenv(KEYCLOAK_REALM); + + if (authUrl == null || realm == null) { + throw new RuntimeException( + "KEYCLOAK_AUTH_ENABLED is true but KEYCLOAK_AUTH_URL or KEYCLOAK_REALM is not set. " + + "Please configure these environment variables."); + } + + // Construct the Keycloak userinfo endpoint URL + // This endpoint validates bearer tokens and returns 200 for valid tokens, 401 + // for invalid + String userinfoEndpoint = authUrl.replaceAll("/+$", "") + "/realms/" + realm + + "/protocol/openid-connect/userinfo"; + + // Configure ForwardAuth middleware + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); + + // Forward the Authorization header to Keycloak + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authResponseHeaders", + "Authorization"); + + // Trust forwarded headers + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.trustForwardHeader", "true"); + } + } From 77329b4ce945f30d4c1bc45889eeb49cbfbde560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:40:35 +0000 Subject: [PATCH 36/41] stop setting traefik on for all containers --- .../stack/services/TraefikService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 9bc63dc3..c88a932f 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -83,15 +83,19 @@ public void addStackServiceToReverseProxy(ContainerService service) { ServiceSpec serviceSpec = service.getServiceSpec(); Map existingLabels = serviceSpec.getLabels(); final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); - labels.put("traefik.enable", "true"); // Check if Keycloak authentication is enabled globally boolean authEnabled = isKeycloakAuthEnabled(); String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; + // Track if any endpoints with external paths were found + final boolean[] hasExternalEndpoints = { false }; + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { + hasExternalEndpoints[0] = true; + String serviceName = service.getContainerName(); String routerName = serviceName + "_" + endpointName; String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); @@ -114,10 +118,15 @@ public void addStackServiceToReverseProxy(ContainerService service) { } }); - // If auth is enabled, configure the ForwardAuth middleware globally for this - // Traefik instance - if (authEnabled) { - configureKeycloakAuthMiddleware(labels); + // Only enable Traefik for services that have external endpoints + if (hasExternalEndpoints[0]) { + labels.put("traefik.enable", "true"); + + // If auth is enabled, configure the ForwardAuth middleware globally for this + // Traefik instance + if (authEnabled) { + configureKeycloakAuthMiddleware(labels); + } } // Set labels on the service spec after they've been populated From 200fe5fee7c680f3d7471464f74ef7ef640d0f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:41:09 +0000 Subject: [PATCH 37/41] attach request headers to forward auth not response headers --- .../java/com/cmclinnovations/stack/services/TraefikService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index c88a932f..55a5c34e 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -165,7 +165,7 @@ private void configureKeycloakAuthMiddleware(Map labels) { labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); // Forward the Authorization header to Keycloak - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authResponseHeaders", + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authRequestHeaders", "Authorization"); // Trust forwarded headers From aaad30c52b687bee6e01fe3bc6da8afc66114283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:42:11 +0000 Subject: [PATCH 38/41] necessary env vars for middleware proxy --- stack-manager/docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index f2536823..52f6ad8d 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -4,6 +4,9 @@ services: environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" + KEYCLOAK_AUTH_ENABLED: "${KEYCLOAK_AUTH_ENABLED-false}" + KEYCLOAK_AUTH_URL: "${KEYCLOAK_AUTH_URL-}" + KEYCLOAK_REALM: "${KEYCLOAK_REALM-}" volumes: - jdbc_drivers:/jdbc - ./inputs/data:/inputs/data From e0d9e8b3d6e34104606493aa5b9d76efc5a7d63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 16:55:21 +0000 Subject: [PATCH 39/41] forwardauth config --- forwardauth/.env | 6 ++++++ forwardauth/bingo.log | 3 +++ forwardauth/compose.yml | 28 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 forwardauth/.env create mode 100644 forwardauth/bingo.log create mode 100644 forwardauth/compose.yml diff --git a/forwardauth/.env b/forwardauth/.env new file mode 100644 index 00000000..d4e8641e --- /dev/null +++ b/forwardauth/.env @@ -0,0 +1,6 @@ +ENCRYPTION_KEY=f57d73478dedc596cf41679568c62986 +CLIENT_SECRET=7awzFYIlK3io3ipSuJd07aXxShp9p9Vp +CLIENT_ID=bingus +PROVIDER_URI=https://dev.theworldavatar.io/realms/twa-test +SIGNING_SECRET=security_is_hard +STACK_NAME=bingus \ No newline at end of file diff --git a/forwardauth/bingo.log b/forwardauth/bingo.log new file mode 100644 index 00000000..c446640b --- /dev/null +++ b/forwardauth/bingo.log @@ -0,0 +1,3 @@ + +time="2026-02-05T18:06:17Z" level=debug msg="Authenticate request" headers="map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[en-IE,en-GB;q=0.9,en;q=0.8] Cache-Control:[max-age=0] Cookie:[_forward_auth_csrf=98b60036f1373bce89d3af6249b537b5] Dnt:[1] Sec-Ch-Ua:[\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Microsoft Edge\";v=\"144\"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:[\"Windows\"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[cross-site] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0] X-Forwarded-For:[10.0.0.2] X-Forwarded-Host:[localhost:1916] X-Forwarded-Port:[1916] X-Forwarded-Proto:[http] X-Forwarded-Server:[bingus-traefik] X-Real-Ip:[10.0.0.2]]" rule=default source_ip=10.0.0.2 +time="2026-02-05T18:06:17Z" level=debug msg="sending CSRF cookie and a redirect to OIDC login" source_ip=10.0.0.2 diff --git a/forwardauth/compose.yml b/forwardauth/compose.yml new file mode 100644 index 00000000..4e64cd19 --- /dev/null +++ b/forwardauth/compose.yml @@ -0,0 +1,28 @@ +services: + forwardauth: + image: mesosphere/traefik-forward-auth + networks: + - stack + environment: + - SECRET=${SIGNING_SECRET} + - PROVIDER_URI=${PROVIDER_URI} + - CLIENT_ID=${CLIENT_ID} + - CLIENT_SECRET=${CLIENT_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + labels: + - "traefik.enable=true" + - "traefik.http.services.forwardauth.loadbalancer.server.port=4181" + - "traefik.http.routers.forwardauth.entrypoints=web" + # Router for OAuth callback endpoint - middleware will detect and process callback + + - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)" + # Middleware definition used by other services + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://forwardauth:4181" + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User" + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.trustForwardHeader=true" + +networks: + stack: + name: ${STACK_NAME} + driver: overlay + external: true From 6525d18de732faf8ca0dc3560188576ec4757605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 17:02:03 +0000 Subject: [PATCH 40/41] generalise traefikservice to auth with another authprovider --- .../stack/services/TraefikService.java | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 55a5c34e..c5dc2fac 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -27,11 +27,9 @@ public class TraefikService extends ContainerService implements ReverseProxyServ private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; - // Keycloak authentication configuration - private static final String KEYCLOAK_AUTH_ENABLED = "KEYCLOAK_AUTH_ENABLED"; - private static final String KEYCLOAK_AUTH_URL = "KEYCLOAK_AUTH_URL"; - private static final String KEYCLOAK_REALM = "KEYCLOAK_REALM"; - private static final String AUTH_MIDDLEWARE_NAME = "keycloak-auth"; + // Forward authentication middleware name (defined by the forwardauth service) + private static final String AUTH_ENABLED = "AUTH_ENABLED"; + private static final String AUTH_MIDDLEWARE_NAME = "traefik-forward-auth"; public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); @@ -84,8 +82,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { Map existingLabels = serviceSpec.getLabels(); final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); - // Check if Keycloak authentication is enabled globally - boolean authEnabled = isKeycloakAuthEnabled(); + // Check if authentication is enabled globally + // The forwardauth service defines the middleware that handles OAuth with + // Keycloak + boolean authEnabled = isAuthEnabled(); String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; // Track if any endpoints with external paths were found @@ -122,11 +122,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { if (hasExternalEndpoints[0]) { labels.put("traefik.enable", "true"); - // If auth is enabled, configure the ForwardAuth middleware globally for this - // Traefik instance - if (authEnabled) { - configureKeycloakAuthMiddleware(labels); - } + // Note: The traefik-forward-auth middleware is defined by the forwardauth + // service + // Services that need authentication simply reference this middleware in their + // router config } // Set labels on the service spec after they've been populated @@ -134,42 +133,13 @@ public void addStackServiceToReverseProxy(ContainerService service) { } /** - * Checks if Keycloak authentication is enabled via environment variable. + * Checks if authentication is enabled via environment variable. + * When enabled, services will use the traefik-forward-auth middleware + * that is defined and configured by the forwardauth service. */ - private boolean isKeycloakAuthEnabled() { - String enabled = System.getenv(KEYCLOAK_AUTH_ENABLED); + private boolean isAuthEnabled() { + String enabled = System.getenv(AUTH_ENABLED); return "true".equalsIgnoreCase(enabled); } - /** - * Configures the Keycloak ForwardAuth middleware on the Traefik service. - * This middleware will be applied to all routers that reference it. - */ - private void configureKeycloakAuthMiddleware(Map labels) { - String authUrl = System.getenv(KEYCLOAK_AUTH_URL); - String realm = System.getenv(KEYCLOAK_REALM); - - if (authUrl == null || realm == null) { - throw new RuntimeException( - "KEYCLOAK_AUTH_ENABLED is true but KEYCLOAK_AUTH_URL or KEYCLOAK_REALM is not set. " + - "Please configure these environment variables."); - } - - // Construct the Keycloak userinfo endpoint URL - // This endpoint validates bearer tokens and returns 200 for valid tokens, 401 - // for invalid - String userinfoEndpoint = authUrl.replaceAll("/+$", "") + "/realms/" + realm - + "/protocol/openid-connect/userinfo"; - - // Configure ForwardAuth middleware - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); - - // Forward the Authorization header to Keycloak - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authRequestHeaders", - "Authorization"); - - // Trust forwarded headers - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.trustForwardHeader", "true"); - } - } From dcd2eed877fad1d63a2578024ba06c837ba46b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 17:06:46 +0000 Subject: [PATCH 41/41] oprional reverse proxy type --- .../src/main/java/com/cmclinnovations/stack/StackConfig.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index fc97f92a..5fbe8127 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import com.cmclinnovations.stack.clients.core.StackHost; import com.cmclinnovations.stack.services.NginxService; @@ -33,7 +32,7 @@ private enum Selector { private final Boolean isolated = false; @JsonProperty("reverseProxy") - private final Optional reverseProxy = Optional.empty(); + private String reverseProxy; @JsonProperty("hostName") private void setHostName(String hostName) { @@ -61,6 +60,6 @@ public boolean isIsolated() { } public String getReverseProxyName() { - return reverseProxy.orElse(NginxService.TYPE); + return reverseProxy != null ? reverseProxy : NginxService.TYPE; } }