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:
@@ -1,6 +1,7 @@
|
|||||||
package com.cameleer.server.app.storage;
|
package com.cameleer.server.app.storage;
|
||||||
|
|
||||||
import com.cameleer.server.core.runtime.Deployment;
|
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.DeploymentRepository;
|
||||||
import com.cameleer.server.core.runtime.DeploymentStatus;
|
import com.cameleer.server.core.runtime.DeploymentStatus;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
@@ -21,7 +22,7 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
|||||||
private static final String SELECT_COLS =
|
private static final String SELECT_COLS =
|
||||||
"id, app_id, app_version_id, environment_id, status, target_state, deployment_strategy, " +
|
"id, app_id, app_version_id, environment_id, status, target_state, deployment_strategy, " +
|
||||||
"replica_states, deploy_stage, container_id, container_name, error_message, " +
|
"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 JdbcTemplate jdbc;
|
||||||
private final ObjectMapper objectMapper;
|
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) {
|
public Optional<Deployment> findByContainerId(String containerId) {
|
||||||
var results = jdbc.query(
|
var results = jdbc.query(
|
||||||
"SELECT " + SELECT_COLS + " FROM deployments WHERE replica_states::text LIKE ? " +
|
"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);
|
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(
|
return new Deployment(
|
||||||
UUID.fromString(rs.getString("id")),
|
UUID.fromString(rs.getString("id")),
|
||||||
UUID.fromString(rs.getString("app_id")),
|
UUID.fromString(rs.getString("app_id")),
|
||||||
@@ -172,7 +191,7 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
|||||||
rs.getString("container_name"),
|
rs.getString("container_name"),
|
||||||
rs.getString("error_message"),
|
rs.getString("error_message"),
|
||||||
resolvedConfig,
|
resolvedConfig,
|
||||||
null, // deployedConfigSnapshot — wired in Task 1.4
|
deployedConfigSnapshot,
|
||||||
deployedAt != null ? deployedAt.toInstant() : null,
|
deployedAt != null ? deployedAt.toInstant() : null,
|
||||||
stoppedAt != null ? stoppedAt.toInstant() : null,
|
stoppedAt != null ? stoppedAt.toInstant() : null,
|
||||||
rs.getTimestamp("created_at").toInstant()
|
rs.getTimestamp("created_at").toInstant()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user