feat: implement PostgreSQL repositories for runtime management

- PostgresEnvironmentRepository, PostgresAppRepository
- PostgresAppVersionRepository, PostgresDeploymentRepository
- RuntimeBeanConfig wiring repositories, services, and async executor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-07 23:43:35 +02:00
parent 55068ff625
commit 585e078667
5 changed files with 381 additions and 0 deletions

View File

@@ -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.
* <p>
* 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;
}
}

View File

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

View File

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

View File

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

View File

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