diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java new file mode 100644 index 00000000..104d83b2 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java @@ -0,0 +1,76 @@ +package com.cameleer3.server.app.config; + +import com.cameleer3.server.app.storage.PostgresAppRepository; +import com.cameleer3.server.app.storage.PostgresAppVersionRepository; +import com.cameleer3.server.app.storage.PostgresDeploymentRepository; +import com.cameleer3.server.app.storage.PostgresEnvironmentRepository; +import com.cameleer3.server.core.runtime.AppRepository; +import com.cameleer3.server.core.runtime.AppService; +import com.cameleer3.server.core.runtime.AppVersionRepository; +import com.cameleer3.server.core.runtime.DeploymentRepository; +import com.cameleer3.server.core.runtime.DeploymentService; +import com.cameleer3.server.core.runtime.EnvironmentRepository; +import com.cameleer3.server.core.runtime.EnvironmentService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +/** + * Creates runtime management beans: repositories, services, and async executor. + *

+ * Follows the established pattern: core module plain class, app module bean config. + */ +@Configuration +public class RuntimeBeanConfig { + + @Bean + public EnvironmentRepository environmentRepository(JdbcTemplate jdbc) { + return new PostgresEnvironmentRepository(jdbc); + } + + @Bean + public AppRepository appRepository(JdbcTemplate jdbc) { + return new PostgresAppRepository(jdbc); + } + + @Bean + public AppVersionRepository appVersionRepository(JdbcTemplate jdbc) { + return new PostgresAppVersionRepository(jdbc); + } + + @Bean + public DeploymentRepository deploymentRepository(JdbcTemplate jdbc) { + return new PostgresDeploymentRepository(jdbc); + } + + @Bean + public EnvironmentService environmentService(EnvironmentRepository repo) { + return new EnvironmentService(repo); + } + + @Bean + public AppService appService(AppRepository appRepo, AppVersionRepository versionRepo, + @Value("${cameleer.runtime.jar-storage-path:/data/jars}") String jarStoragePath) { + return new AppService(appRepo, versionRepo, jarStoragePath); + } + + @Bean + public DeploymentService deploymentService(DeploymentRepository deployRepo, AppService appService, EnvironmentService envService) { + return new DeploymentService(deployRepo, appService, envService); + } + + @Bean(name = "deploymentExecutor") + public Executor deploymentTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(25); + executor.setThreadNamePrefix("deploy-"); + executor.initialize(); + return executor; + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java new file mode 100644 index 00000000..2d5ec3e9 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java @@ -0,0 +1,66 @@ +package com.cameleer3.server.app.storage; + +import com.cameleer3.server.core.runtime.App; +import com.cameleer3.server.core.runtime.AppRepository; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class PostgresAppRepository implements AppRepository { + + private final JdbcTemplate jdbc; + + public PostgresAppRepository(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public List findByEnvironmentId(UUID environmentId) { + return jdbc.query( + "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE environment_id = ? ORDER BY created_at", + (rs, rowNum) -> mapRow(rs), environmentId); + } + + @Override + public Optional findById(UUID id) { + var results = jdbc.query( + "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE id = ?", + (rs, rowNum) -> mapRow(rs), id); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public Optional findByEnvironmentIdAndSlug(UUID environmentId, String slug) { + var results = jdbc.query( + "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE environment_id = ? AND slug = ?", + (rs, rowNum) -> mapRow(rs), environmentId, slug); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public UUID create(UUID environmentId, String slug, String displayName) { + UUID id = UUID.randomUUID(); + jdbc.update("INSERT INTO apps (id, environment_id, slug, display_name) VALUES (?, ?, ?, ?)", + id, environmentId, slug, displayName); + return id; + } + + @Override + public void delete(UUID id) { + jdbc.update("DELETE FROM apps WHERE id = ?", id); + } + + private App mapRow(ResultSet rs) throws SQLException { + return new App( + UUID.fromString(rs.getString("id")), + UUID.fromString(rs.getString("environment_id")), + rs.getString("slug"), + rs.getString("display_name"), + rs.getTimestamp("created_at").toInstant() + ); + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java new file mode 100644 index 00000000..1ca89d17 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java @@ -0,0 +1,66 @@ +package com.cameleer3.server.app.storage; + +import com.cameleer3.server.core.runtime.AppVersion; +import com.cameleer3.server.core.runtime.AppVersionRepository; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class PostgresAppVersionRepository implements AppVersionRepository { + + private final JdbcTemplate jdbc; + + public PostgresAppVersionRepository(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public List findByAppId(UUID appId) { + return jdbc.query( + "SELECT id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes, uploaded_at FROM app_versions WHERE app_id = ? ORDER BY version DESC", + (rs, rowNum) -> mapRow(rs), appId); + } + + @Override + public Optional findById(UUID id) { + var results = jdbc.query( + "SELECT id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes, uploaded_at FROM app_versions WHERE id = ?", + (rs, rowNum) -> mapRow(rs), id); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public int findMaxVersion(UUID appId) { + Integer max = jdbc.queryForObject( + "SELECT COALESCE(MAX(version), 0) FROM app_versions WHERE app_id = ?", + Integer.class, appId); + return max != null ? max : 0; + } + + @Override + public UUID create(UUID appId, int version, String jarPath, String jarChecksum, String jarFilename, Long jarSizeBytes) { + UUID id = UUID.randomUUID(); + jdbc.update("INSERT INTO app_versions (id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes) VALUES (?, ?, ?, ?, ?, ?, ?)", + id, appId, version, jarPath, jarChecksum, jarFilename, jarSizeBytes); + return id; + } + + private AppVersion mapRow(ResultSet rs) throws SQLException { + Long sizeBytes = rs.getLong("jar_size_bytes"); + if (rs.wasNull()) sizeBytes = null; + return new AppVersion( + UUID.fromString(rs.getString("id")), + UUID.fromString(rs.getString("app_id")), + rs.getInt("version"), + rs.getString("jar_path"), + rs.getString("jar_checksum"), + rs.getString("jar_filename"), + sizeBytes, + rs.getTimestamp("uploaded_at").toInstant() + ); + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java new file mode 100644 index 00000000..1aad0e38 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java @@ -0,0 +1,95 @@ +package com.cameleer3.server.app.storage; + +import com.cameleer3.server.core.runtime.Deployment; +import com.cameleer3.server.core.runtime.DeploymentRepository; +import com.cameleer3.server.core.runtime.DeploymentStatus; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class PostgresDeploymentRepository implements DeploymentRepository { + + private final JdbcTemplate jdbc; + + public PostgresDeploymentRepository(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public List findByAppId(UUID appId) { + return jdbc.query( + "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE app_id = ? ORDER BY created_at DESC", + (rs, rowNum) -> mapRow(rs), appId); + } + + @Override + public List findByEnvironmentId(UUID environmentId) { + return jdbc.query( + "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE environment_id = ? ORDER BY created_at DESC", + (rs, rowNum) -> mapRow(rs), environmentId); + } + + @Override + public Optional findById(UUID id) { + var results = jdbc.query( + "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE id = ?", + (rs, rowNum) -> mapRow(rs), id); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public Optional findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId) { + var results = jdbc.query( + "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE app_id = ? AND environment_id = ? AND status IN ('STARTING', 'RUNNING') ORDER BY created_at DESC LIMIT 1", + (rs, rowNum) -> mapRow(rs), appId, environmentId); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName) { + UUID id = UUID.randomUUID(); + jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name) VALUES (?, ?, ?, ?, ?)", + id, appId, appVersionId, environmentId, containerName); + return id; + } + + @Override + public void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage) { + jdbc.update("UPDATE deployments SET status = ?, container_id = ?, error_message = ? WHERE id = ?", + status.name(), containerId, errorMessage, id); + } + + @Override + public void markDeployed(UUID id) { + jdbc.update("UPDATE deployments SET deployed_at = now() WHERE id = ?", id); + } + + @Override + public void markStopped(UUID id) { + jdbc.update("UPDATE deployments SET stopped_at = now() WHERE id = ?", id); + } + + private Deployment mapRow(ResultSet rs) throws SQLException { + Timestamp deployedAt = rs.getTimestamp("deployed_at"); + Timestamp stoppedAt = rs.getTimestamp("stopped_at"); + return new Deployment( + UUID.fromString(rs.getString("id")), + UUID.fromString(rs.getString("app_id")), + UUID.fromString(rs.getString("app_version_id")), + UUID.fromString(rs.getString("environment_id")), + DeploymentStatus.valueOf(rs.getString("status")), + rs.getString("container_id"), + rs.getString("container_name"), + rs.getString("error_message"), + deployedAt != null ? deployedAt.toInstant() : null, + stoppedAt != null ? stoppedAt.toInstant() : null, + rs.getTimestamp("created_at").toInstant() + ); + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java new file mode 100644 index 00000000..4d46200c --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java @@ -0,0 +1,78 @@ +package com.cameleer3.server.app.storage; + +import com.cameleer3.server.core.runtime.Environment; +import com.cameleer3.server.core.runtime.EnvironmentRepository; +import com.cameleer3.server.core.runtime.EnvironmentStatus; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class PostgresEnvironmentRepository implements EnvironmentRepository { + + private final JdbcTemplate jdbc; + + public PostgresEnvironmentRepository(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public List findAll() { + return jdbc.query("SELECT id, slug, display_name, status, created_at FROM environments ORDER BY created_at", + (rs, rowNum) -> mapRow(rs)); + } + + @Override + public Optional findById(UUID id) { + var results = jdbc.query( + "SELECT id, slug, display_name, status, created_at FROM environments WHERE id = ?", + (rs, rowNum) -> mapRow(rs), id); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public Optional findBySlug(String slug) { + var results = jdbc.query( + "SELECT id, slug, display_name, status, created_at FROM environments WHERE slug = ?", + (rs, rowNum) -> mapRow(rs), slug); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + @Override + public UUID create(String slug, String displayName) { + UUID id = UUID.randomUUID(); + jdbc.update("INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)", + id, slug, displayName); + return id; + } + + @Override + public void updateDisplayName(UUID id, String displayName) { + jdbc.update("UPDATE environments SET display_name = ?, updated_at = now() WHERE id = ?", + displayName, id); + } + + @Override + public void updateStatus(UUID id, EnvironmentStatus status) { + jdbc.update("UPDATE environments SET status = ?, updated_at = now() WHERE id = ?", + status.name(), id); + } + + @Override + public void delete(UUID id) { + jdbc.update("DELETE FROM environments WHERE id = ?", id); + } + + private Environment mapRow(ResultSet rs) throws SQLException { + return new Environment( + UUID.fromString(rs.getString("id")), + rs.getString("slug"), + rs.getString("display_name"), + EnvironmentStatus.valueOf(rs.getString("status")), + rs.getTimestamp("created_at").toInstant() + ); + } +}