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}