diff --git a/pom.xml b/pom.xml
index 636812f..4acdeb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,18 @@
spring-boot-starter-actuator
+
+
+ com.github.docker-java
+ docker-java-core
+ 3.4.1
+
+
+ com.github.docker-java
+ docker-java-transport-zerodep
+ 3.4.1
+
+
org.springframework.boot
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/DisabledTenantProvisioner.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/DisabledTenantProvisioner.java
new file mode 100644
index 0000000..a4f88dc
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/DisabledTenantProvisioner.java
@@ -0,0 +1,19 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DisabledTenantProvisioner implements TenantProvisioner {
+ private static final Logger log = LoggerFactory.getLogger(DisabledTenantProvisioner.class);
+
+ @Override public boolean isAvailable() { return false; }
+ @Override public ProvisionResult provision(TenantProvisionRequest request) {
+ log.warn("Provisioning disabled — no Docker socket or K8s detected");
+ return ProvisionResult.fail("Provisioning not available");
+ }
+ @Override public void start(String slug) { log.warn("Cannot start: provisioning disabled"); }
+ @Override public void stop(String slug) { log.warn("Cannot stop: provisioning disabled"); }
+ @Override public void remove(String slug) { log.warn("Cannot remove: provisioning disabled"); }
+ @Override public ServerStatus getStatus(String slug) { return ServerStatus.notFound(); }
+ @Override public String getServerEndpoint(String slug) { return null; }
+}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java
new file mode 100644
index 0000000..0c7803b
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java
@@ -0,0 +1,21 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+import com.github.dockerjava.core.DockerClientConfig;
+
+public class DockerTenantProvisioner implements TenantProvisioner {
+ private final DockerClientConfig config;
+ private final ProvisioningProperties props;
+
+ public DockerTenantProvisioner(DockerClientConfig config, ProvisioningProperties props) {
+ this.config = config;
+ this.props = props;
+ }
+
+ @Override public boolean isAvailable() { return true; }
+ @Override public ProvisionResult provision(TenantProvisionRequest request) { throw new UnsupportedOperationException("Not yet implemented"); }
+ @Override public void start(String slug) { throw new UnsupportedOperationException("Not yet implemented"); }
+ @Override public void stop(String slug) { throw new UnsupportedOperationException("Not yet implemented"); }
+ @Override public void remove(String slug) { throw new UnsupportedOperationException("Not yet implemented"); }
+ @Override public ServerStatus getStatus(String slug) { return ServerStatus.notFound(); }
+ @Override public String getServerEndpoint(String slug) { return "http://cameleer-server-" + slug + ":8081"; }
+}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisionResult.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisionResult.java
new file mode 100644
index 0000000..9e75bdc
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisionResult.java
@@ -0,0 +1,14 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+public record ProvisionResult(
+ boolean success,
+ String serverEndpoint,
+ String error
+) {
+ public static ProvisionResult ok(String endpoint) {
+ return new ProvisionResult(true, endpoint, null);
+ }
+ public static ProvisionResult fail(String error) {
+ return new ProvisionResult(false, null, error);
+ }
+}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisioningProperties.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisioningProperties.java
new file mode 100644
index 0000000..e01870d
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/ProvisioningProperties.java
@@ -0,0 +1,17 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "cameleer.provisioning")
+public record ProvisioningProperties(
+ String serverImage,
+ String serverUiImage,
+ String networkName,
+ String traefikNetwork,
+ String publicHost,
+ String publicProtocol,
+ String datasourceUrl,
+ String oidcIssuerUri,
+ String oidcJwkSetUri,
+ String corsOrigins
+) {}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/ServerStatus.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/ServerStatus.java
new file mode 100644
index 0000000..8b2defd
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/ServerStatus.java
@@ -0,0 +1,22 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+public record ServerStatus(
+ State state,
+ String containerId,
+ String error
+) {
+ public enum State { RUNNING, STOPPED, NOT_FOUND, ERROR }
+
+ public static ServerStatus running(String containerId) {
+ return new ServerStatus(State.RUNNING, containerId, null);
+ }
+ public static ServerStatus stopped(String containerId) {
+ return new ServerStatus(State.STOPPED, containerId, null);
+ }
+ public static ServerStatus notFound() {
+ return new ServerStatus(State.NOT_FOUND, null, null);
+ }
+ public static ServerStatus error(String error) {
+ return new ServerStatus(State.ERROR, null, error);
+ }
+}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java
new file mode 100644
index 0000000..553a880
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java
@@ -0,0 +1,10 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+import java.util.UUID;
+
+public record TenantProvisionRequest(
+ UUID tenantId,
+ String slug,
+ String tier,
+ String licenseToken
+) {}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisioner.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisioner.java
new file mode 100644
index 0000000..72dca4d
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisioner.java
@@ -0,0 +1,11 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+public interface TenantProvisioner {
+ boolean isAvailable();
+ ProvisionResult provision(TenantProvisionRequest request);
+ void start(String slug);
+ void stop(String slug);
+ void remove(String slug);
+ ServerStatus getStatus(String slug);
+ String getServerEndpoint(String slug);
+}
diff --git a/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionerAutoConfig.java b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionerAutoConfig.java
new file mode 100644
index 0000000..b436494
--- /dev/null
+++ b/src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionerAutoConfig.java
@@ -0,0 +1,31 @@
+package net.siegeln.cameleer.saas.provisioning;
+
+import com.github.dockerjava.core.DefaultDockerClientConfig;
+import com.github.dockerjava.core.DockerClientConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@Configuration
+@EnableConfigurationProperties(ProvisioningProperties.class)
+public class TenantProvisionerAutoConfig {
+ private static final Logger log = LoggerFactory.getLogger(TenantProvisionerAutoConfig.class);
+
+ @Bean
+ TenantProvisioner tenantProvisioner(ProvisioningProperties props) {
+ if (Files.exists(Path.of("/var/run/docker.sock"))) {
+ log.info("Docker socket detected — enabling Docker tenant provisioner");
+ DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
+ .withDockerHost("unix:///var/run/docker.sock")
+ .build();
+ return new DockerTenantProvisioner(config, props);
+ }
+ log.info("No Docker socket — tenant provisioning disabled");
+ return new DisabledTenantProvisioner();
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 61b3351..4df2043 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -41,3 +41,14 @@ cameleer:
spa-client-id: ${LOGTO_SPA_CLIENT_ID:}
audience: ${CAMELEER_OIDC_AUDIENCE:https://api.cameleer.local}
server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
+ provisioning:
+ server-image: ${CAMELEER_SERVER_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server:latest}
+ server-ui-image: ${CAMELEER_SERVER_UI_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server-ui:latest}
+ network-name: ${CAMELEER_NETWORK:cameleer-saas_cameleer}
+ traefik-network: ${CAMELEER_TRAEFIK_NETWORK:cameleer-traefik}
+ public-host: ${PUBLIC_HOST:localhost}
+ public-protocol: ${PUBLIC_PROTOCOL:https}
+ datasource-url: ${CAMELEER_SERVER_DB_URL:jdbc:postgresql://postgres:5432/cameleer3}
+ oidc-issuer-uri: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost}/oidc
+ oidc-jwk-set-uri: http://logto:3001/oidc/jwks
+ cors-origins: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost}