Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
354a579
add-traefik-support: Added a stack config setting to allow the revers…
gpeb2 Jan 6, 2026
4f645cb
add-traefik-support: Bumped stack version.
gpeb2 Jan 6, 2026
1124705
add-traefik-support: Added "empty" `TraefikService` class.
gpeb2 Jan 6, 2026
84a1fd5
get debugger working
Ushcode Jan 16, 2026
52332ce
traefik service config file
Ushcode Jan 16, 2026
461b7c6
addreverseproxy method in stack.java
Ushcode Jan 16, 2026
88aca33
remove snashot
Ushcode Jan 20, 2026
711978e
fix version
Ushcode Jan 20, 2026
53e725c
rm redundant import
Ushcode Jan 20, 2026
7667dd9
replace deprecated docker api call
Ushcode Jan 20, 2026
dc8dee1
add-traefik-support: Added a stack config setting to allow the revers…
gpeb2 Jan 6, 2026
d04d90d
add-traefik-support: Bumped stack version.
gpeb2 Jan 6, 2026
70871f6
add-traefik-support: Added "empty" `TraefikService` class.
gpeb2 Jan 6, 2026
e882d97
get debugger working
Ushcode Jan 16, 2026
14bb232
traefik service config file
Ushcode Jan 16, 2026
f0d7239
addreverseproxy method in stack.java
Ushcode Jan 16, 2026
8bc9305
rm redundant import
Ushcode Jan 20, 2026
e81af7a
replace deprecated docker api call
Ushcode Jan 20, 2026
9e3ddfb
Merge branch 'add-traefik-support' of https://github.com/TheWorldAvat…
Ushcode Jan 20, 2026
26f6c7d
rm duplicate docker socket mount for traefik
Ushcode Jan 21, 2026
5784de3
give traefik service clasee a type field
Ushcode Jan 21, 2026
1b4617e
rm bad traefik service starter method
Ushcode Jan 21, 2026
6b50203
fix the reverseProxy string field in StackConfig
Ushcode Jan 21, 2026
55d4d6b
rm an unused import
Ushcode Jan 21, 2026
702924e
organise imports
Ushcode Jan 21, 2026
fbaa083
add a traefik config file
Ushcode Jan 21, 2026
9224d02
implement traefik config method
Ushcode Jan 21, 2026
0c469a5
format and do imports on save for everyone
Ushcode Jan 22, 2026
a95a58a
fix the provider in traefik.yaml
Ushcode Jan 22, 2026
d817ff9
merge the labels
Ushcode Jan 26, 2026
5640561
better name for a method
Ushcode Jan 26, 2026
01fe367
loads of label stuff
Ushcode Jan 26, 2026
8e92c93
customisable stack port
Ushcode Jan 26, 2026
72e0702
rm the bad host port casting
Ushcode Jan 26, 2026
26af8d8
reproduce specific port functionality
Ushcode Jan 28, 2026
2755320
try an auth setup with keycloak env vars
Ushcode Jan 28, 2026
77329b4
stop setting traefik on for all containers
Ushcode Jan 29, 2026
200fe5f
attach request headers to forward auth not response headers
Ushcode Jan 29, 2026
aaad30c
necessary env vars for middleware proxy
Ushcode Jan 29, 2026
e0d9e8b
forwardauth config
Ushcode Feb 6, 2026
6525d18
generalise traefikservice to auth with another authprovider
Ushcode Feb 6, 2026
dcd2eed
oprional reverse proxy type
Ushcode Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions forwardauth/.env
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions forwardauth/bingo.log
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions forwardauth/compose.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions stack-clients/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"[java]": {
"editor.formatOnSave": true
}
}
2 changes: 1 addition & 1 deletion stack-clients/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions stack-clients/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@

<groupId>com.cmclinnovations</groupId>
<artifactId>stack-clients</artifactId>
<version>1.56.2</version>
<version>1.57.0-traefik-support-SNAPSHOT</version>

<name>Stack Clients</name>
<url>https://theworldavatar.io</url>

<parent>
<groupId>uk.ac.cam.cares.jps</groupId>
<artifactId>jps-parent-pom</artifactId>
<version>2.3.2</version>
<version>2.4.0</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -115,7 +117,6 @@ public String executeSimpleCommand(String containerId, String... cmd) {
.withOutputStream(outputStream)
.withErrorStream(outputStream)
.exec();
String output = outputStream.toString();
return execId;
}

Expand Down Expand Up @@ -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<Frame> result = execStartCmd
.exec(new ResultCallback.Adapter<Frame>() {
@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.",
Expand Down Expand Up @@ -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<String, List<String>> convertToConfigFilterMap(String configName, Map<String, String> labelMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> 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()
Expand All @@ -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<String, String> containerLabels = new HashMap<>();
if (containerSpec.getLabels() != null) {
containerLabels.putAll(containerSpec.getLabels());
}
containerLabels.putAll(StackClient.getStackNameLabelMap());
containerSpec.withLabels(containerLabels);

interpolateEnvironmentVariables(containerSpec);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -63,22 +59,7 @@ 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<PortConfig> ports = endpointSpec.getPorts();
if (null != ports) {
ports.stream()
.filter(port -> port.getTargetPort() == 80)
.forEach(port -> port.withPublishedPort(Integer.parseInt(externalPort)));
}
}
}
}

public void addService(ContainerService service) {
public void addStackServiceToReverseProxy(ContainerService service) {

NgxConfig locationConfigOut = new NgxConfig();

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 addService(ContainerService 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<PortConfig> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,17 @@ public <S extends Service> 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.addStackServiceToReverseProxy(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);
Expand Down
Loading