feat: add TenantProvisioner interface with auto-detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
12
pom.xml
12
pom.xml
@@ -80,6 +80,18 @@
|
|||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
</dependency>
|
</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 -->
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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"; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,3 +41,14 @@ cameleer:
|
|||||||
spa-client-id: ${LOGTO_SPA_CLIENT_ID:}
|
spa-client-id: ${LOGTO_SPA_CLIENT_ID:}
|
||||||
audience: ${CAMELEER_OIDC_AUDIENCE:https://api.cameleer.local}
|
audience: ${CAMELEER_OIDC_AUDIENCE:https://api.cameleer.local}
|
||||||
server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user