diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java index 1715e008..264baf60 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java @@ -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 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() diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java new file mode 100644 index 00000000..2b20ab26 --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java @@ -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(); + } +}