storage(deploy): persist deployed_config_snapshot as JSONB

Wire SELECT_COLS, mapRow deserialization, and saveDeployedConfigSnapshot
update method. Adds PostgresDeploymentRepositoryIT with roundtrip,
null-default, and clear-to-null tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 21:39:04 +02:00
parent 7f9cfc7f18
commit d3e86b9d77
2 changed files with 129 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
package com.cameleer.server.app.storage;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentConfigSnapshot;
import com.cameleer.server.core.runtime.DeploymentRepository;
import com.cameleer.server.core.runtime.DeploymentStatus;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -21,7 +22,7 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
private static final String SELECT_COLS =
"id, app_id, app_version_id, environment_id, status, target_state, deployment_strategy, " +
"replica_states, deploy_stage, container_id, container_name, error_message, " +
"resolved_config, deployed_at, stopped_at, created_at";
"resolved_config, deployed_config_snapshot, deployed_at, stopped_at, created_at";
private final JdbcTemplate jdbc;
private final ObjectMapper objectMapper;
@@ -129,6 +130,15 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
}
}
public void saveDeployedConfigSnapshot(UUID id, DeploymentConfigSnapshot snapshot) {
try {
String json = snapshot != null ? objectMapper.writeValueAsString(snapshot) : null;
jdbc.update("UPDATE deployments SET deployed_config_snapshot = ?::jsonb WHERE id = ?", json, id);
} catch (Exception e) {
throw new RuntimeException("Failed to serialize deployed_config_snapshot", e);
}
}
public Optional<Deployment> findByContainerId(String containerId) {
var results = jdbc.query(
"SELECT " + SELECT_COLS + " FROM deployments WHERE replica_states::text LIKE ? " +
@@ -158,6 +168,15 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
throw new SQLException("Failed to deserialize resolved_config", e);
}
}
DeploymentConfigSnapshot deployedConfigSnapshot = null;
String snapshotJson = rs.getString("deployed_config_snapshot");
if (snapshotJson != null) {
try {
deployedConfigSnapshot = objectMapper.readValue(snapshotJson, DeploymentConfigSnapshot.class);
} catch (Exception e) {
throw new SQLException("Failed to deserialize deployed_config_snapshot", e);
}
}
return new Deployment(
UUID.fromString(rs.getString("id")),
UUID.fromString(rs.getString("app_id")),
@@ -172,7 +191,7 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
rs.getString("container_name"),
rs.getString("error_message"),
resolvedConfig,
null, // deployedConfigSnapshot — wired in Task 1.4
deployedConfigSnapshot,
deployedAt != null ? deployedAt.toInstant() : null,
stoppedAt != null ? stoppedAt.toInstant() : null,
rs.getTimestamp("created_at").toInstant()

View File

@@ -0,0 +1,108 @@
package com.cameleer.server.app.storage;
import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentConfigSnapshot;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
private PostgresDeploymentRepository repository;
private UUID envId;
private UUID appId;
private UUID appVersionId;
@BeforeEach
void setup() {
repository = new PostgresDeploymentRepository(jdbcTemplate, new ObjectMapper());
envId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
envId, "test-env-" + envId, "Test Env");
appId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO apps (id, environment_id, slug, display_name) VALUES (?, ?, ?, ?)",
appId, envId, "app-it-" + appId, "App IT");
appVersionId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO app_versions (id, app_id, version, jar_path, jar_checksum) VALUES (?, ?, ?, ?, ?)",
appVersionId, appId, 1, "/tmp/app.jar", "deadbeef");
}
@AfterEach
void cleanup() {
jdbcTemplate.update("DELETE FROM deployments WHERE app_id = ?", appId);
jdbcTemplate.update("DELETE FROM app_versions WHERE app_id = ?", appId);
jdbcTemplate.update("DELETE FROM apps WHERE id = ?", appId);
jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
}
@Test
void deployedConfigSnapshot_roundtrips() {
// given — create a deployment then store a snapshot
ApplicationConfig agentConfig = new ApplicationConfig();
agentConfig.setApplication("app-it");
agentConfig.setEnvironment("staging");
agentConfig.setVersion(3);
agentConfig.setSamplingRate(0.5);
UUID jarVersionId = UUID.randomUUID();
DeploymentConfigSnapshot snapshot = new DeploymentConfigSnapshot(
jarVersionId,
agentConfig,
Map.of("memoryLimitMb", 1024, "replicas", 2)
);
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container");
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
// when — load it back
Deployment loaded = repository.findById(deploymentId).orElseThrow();
// then
assertThat(loaded.deployedConfigSnapshot()).isNotNull();
assertThat(loaded.deployedConfigSnapshot().jarVersionId()).isEqualTo(jarVersionId);
assertThat(loaded.deployedConfigSnapshot().agentConfig().getSamplingRate()).isEqualTo(0.5);
assertThat(loaded.deployedConfigSnapshot().containerConfig()).containsEntry("memoryLimitMb", 1024);
}
@Test
void deployedConfigSnapshot_nullByDefault() {
// deployments created without a snapshot must return null (not throw)
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-null");
Deployment loaded = repository.findById(deploymentId).orElseThrow();
assertThat(loaded.deployedConfigSnapshot()).isNull();
}
@Test
void deployedConfigSnapshot_canBeClearedToNull() {
UUID jarVersionId = UUID.randomUUID();
DeploymentConfigSnapshot snapshot = new DeploymentConfigSnapshot(
jarVersionId,
new ApplicationConfig(),
Map.of()
);
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-clear");
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
repository.saveDeployedConfigSnapshot(deploymentId, null);
Deployment loaded = repository.findById(deploymentId).orElseThrow();
assertThat(loaded.deployedConfigSnapshot()).isNull();
}
}