feat: add TenantProvisioner interface with auto-detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 21:39:58 +02:00
parent ebba021448
commit 771e9d1081
10 changed files with 168 additions and 0 deletions

12
pom.xml
View File

@@ -80,6 +80,18 @@
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Docker Java (tenant provisioning) -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-zerodep</artifactId>
<version>3.4.1</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -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; }
}

View File

@@ -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"; }
}

View File

@@ -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);
}
}

View File

@@ -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
) {}

View File

@@ -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);
}
}

View File

@@ -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
) {}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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}