feat(deploy): cascade createdBy through Deployment record + service + repo
Appends String createdBy to the Deployment record (after createdAt), updates both with-er methods to pass it through, threads the parameter through DeploymentRepository.create, DeploymentService.createDeployment/promote, and PostgresDeploymentRepository (INSERT + SELECT_COLS + mapRow). DeploymentController passes null as placeholder (Task 4 will resolve from SecurityContextHolder). Covers with PostgresDeploymentRepositoryCreatedByIT verifying round-trip via both createDeployment and promote. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ public class DeploymentController {
|
||||
@RequestBody DeployRequest request) {
|
||||
try {
|
||||
App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id());
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), null);
|
||||
deploymentExecutor.executeAsync(deployment);
|
||||
return ResponseEntity.accepted().body(deployment);
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -129,7 +129,7 @@ public class DeploymentController {
|
||||
Environment targetEnv = environmentService.getBySlug(request.targetEnvironment());
|
||||
// Target must also have the app with the same slug
|
||||
App targetApp = appService.getByEnvironmentAndSlug(targetEnv.id(), appSlug);
|
||||
Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id());
|
||||
Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id(), null);
|
||||
deploymentExecutor.executeAsync(promoted);
|
||||
return ResponseEntity.accepted().body(promoted);
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -22,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_config_snapshot, deployed_at, stopped_at, created_at";
|
||||
"resolved_config, deployed_config_snapshot, deployed_at, stopped_at, created_at, created_by";
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -81,10 +81,10 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName) {
|
||||
public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName, String createdBy) {
|
||||
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);
|
||||
jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name, created_by) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
id, appId, appVersionId, environmentId, containerName, createdBy);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -216,7 +216,8 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
||||
deployedConfigSnapshot,
|
||||
deployedAt != null ? deployedAt.toInstant() : null,
|
||||
stoppedAt != null ? stoppedAt.toInstant() : null,
|
||||
rs.getTimestamp("created_at").toInstant()
|
||||
rs.getTimestamp("created_at").toInstant(),
|
||||
rs.getString("created_by")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class DeploymentStateEvaluatorTest {
|
||||
private Deployment deployment(DeploymentStatus status) {
|
||||
return new Deployment(DEP_ID, APP_ID, UUID.randomUUID(), ENV_ID, status,
|
||||
null, null, List.of(), null, null, "orders-0", null,
|
||||
Map.of(), null, NOW.minusSeconds(60), null, NOW.minusSeconds(120));
|
||||
Map.of(), null, NOW.minusSeconds(60), null, NOW.minusSeconds(120), "test-user");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.cameleer.server.app.storage;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.core.runtime.Deployment;
|
||||
import com.cameleer.server.core.runtime.DeploymentService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PostgresDeploymentRepositoryCreatedByIT extends AbstractPostgresIT {
|
||||
|
||||
@Autowired DeploymentService deploymentService;
|
||||
@Autowired JdbcTemplate jdbc;
|
||||
|
||||
private UUID appId;
|
||||
private UUID envId;
|
||||
private UUID versionId;
|
||||
|
||||
@BeforeEach
|
||||
void seedAppAndVersion() {
|
||||
// Clean up to avoid conflicts across test runs
|
||||
jdbc.update("DELETE FROM deployments");
|
||||
jdbc.update("DELETE FROM app_versions");
|
||||
jdbc.update("DELETE FROM apps");
|
||||
|
||||
envId = jdbc.queryForObject(
|
||||
"SELECT id FROM environments WHERE slug = 'default'", UUID.class);
|
||||
|
||||
// Seed users (alice, bob) — use the bare user_id convention; provider is NOT NULL
|
||||
jdbc.update("INSERT INTO users (user_id, provider) VALUES (?, 'LOCAL') " +
|
||||
"ON CONFLICT (user_id) DO NOTHING", "alice");
|
||||
jdbc.update("INSERT INTO users (user_id, provider) VALUES (?, 'LOCAL') " +
|
||||
"ON CONFLICT (user_id) DO NOTHING", "bob");
|
||||
|
||||
// Seed app
|
||||
appId = UUID.randomUUID();
|
||||
jdbc.update("INSERT INTO apps (id, environment_id, slug, display_name) " +
|
||||
"VALUES (?, ?, 'test-app', 'Test App')",
|
||||
appId, envId);
|
||||
|
||||
// Seed version
|
||||
versionId = UUID.randomUUID();
|
||||
jdbc.update("INSERT INTO app_versions (id, app_id, version, jar_path, jar_checksum) " +
|
||||
"VALUES (?, ?, 1, '/tmp/x.jar', 'abc')",
|
||||
versionId, appId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDeployment_persists_createdBy_and_returns_it() {
|
||||
Deployment d = deploymentService.createDeployment(appId, versionId, envId, "alice");
|
||||
assertThat(d.createdBy()).isEqualTo("alice");
|
||||
String fromDb = jdbc.queryForObject(
|
||||
"SELECT created_by FROM deployments WHERE id = ?", String.class, d.id());
|
||||
assertThat(fromDb).isEqualTo("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void promote_persists_createdBy() {
|
||||
Deployment promoted = deploymentService.promote(appId, versionId, envId, "bob");
|
||||
assertThat(promoted.createdBy()).isEqualTo("bob");
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
||||
null
|
||||
);
|
||||
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container");
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container", null);
|
||||
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
|
||||
|
||||
// when — load it back
|
||||
@@ -80,7 +80,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
||||
@Test
|
||||
void deployedConfigSnapshot_nullByDefault() {
|
||||
// deployments created without a snapshot must return null (not throw)
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-null");
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-null", null);
|
||||
|
||||
Deployment loaded = repository.findById(deploymentId).orElseThrow();
|
||||
|
||||
@@ -90,13 +90,13 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
||||
@Test
|
||||
void deleteFailedByAppAndEnvironment_keepsStoppedAndActive() {
|
||||
// given: one STOPPED (checkpoint), one FAILED, one RUNNING
|
||||
UUID stoppedId = repository.create(appId, appVersionId, envId, "stopped");
|
||||
UUID stoppedId = repository.create(appId, appVersionId, envId, "stopped", null);
|
||||
repository.updateStatus(stoppedId, com.cameleer.server.core.runtime.DeploymentStatus.STOPPED, null, null);
|
||||
|
||||
UUID failedId = repository.create(appId, appVersionId, envId, "failed");
|
||||
UUID failedId = repository.create(appId, appVersionId, envId, "failed", null);
|
||||
repository.updateStatus(failedId, com.cameleer.server.core.runtime.DeploymentStatus.FAILED, null, "boom");
|
||||
|
||||
UUID runningId = repository.create(appId, appVersionId, envId, "running");
|
||||
UUID runningId = repository.create(appId, appVersionId, envId, "running", null);
|
||||
repository.updateStatus(runningId, com.cameleer.server.core.runtime.DeploymentStatus.RUNNING, "c1", null);
|
||||
|
||||
// when
|
||||
@@ -118,7 +118,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
||||
null
|
||||
);
|
||||
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-clear");
|
||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-clear", null);
|
||||
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
|
||||
repository.saveDeployedConfigSnapshot(deploymentId, null);
|
||||
|
||||
|
||||
@@ -22,19 +22,20 @@ public record Deployment(
|
||||
DeploymentConfigSnapshot deployedConfigSnapshot,
|
||||
Instant deployedAt,
|
||||
Instant stoppedAt,
|
||||
Instant createdAt
|
||||
Instant createdAt,
|
||||
String createdBy
|
||||
) {
|
||||
public Deployment withStatus(DeploymentStatus newStatus) {
|
||||
return new Deployment(id, appId, appVersionId, environmentId, newStatus,
|
||||
targetState, deploymentStrategy, replicaStates, deployStage,
|
||||
containerId, containerName, errorMessage, resolvedConfig,
|
||||
deployedConfigSnapshot, deployedAt, stoppedAt, createdAt);
|
||||
deployedConfigSnapshot, deployedAt, stoppedAt, createdAt, createdBy);
|
||||
}
|
||||
|
||||
public Deployment withDeployedConfigSnapshot(DeploymentConfigSnapshot snapshot) {
|
||||
return new Deployment(id, appId, appVersionId, environmentId, status,
|
||||
targetState, deploymentStrategy, replicaStates, deployStage,
|
||||
containerId, containerName, errorMessage, resolvedConfig,
|
||||
snapshot, deployedAt, stoppedAt, createdAt);
|
||||
snapshot, deployedAt, stoppedAt, createdAt, createdBy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public interface DeploymentRepository {
|
||||
Optional<Deployment> findById(UUID id);
|
||||
Optional<Deployment> findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId);
|
||||
Optional<Deployment> findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId);
|
||||
UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName);
|
||||
UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName, String createdBy);
|
||||
void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage);
|
||||
void markDeployed(UUID id);
|
||||
void markStopped(UUID id);
|
||||
|
||||
@@ -23,19 +23,19 @@ public class DeploymentService {
|
||||
public Deployment getById(UUID id) { return deployRepo.findById(id).orElseThrow(() -> new IllegalArgumentException("Deployment not found: " + id)); }
|
||||
|
||||
/** Create a deployment record. Actual container start is handled by DeploymentExecutor (async). */
|
||||
public Deployment createDeployment(UUID appId, UUID appVersionId, UUID environmentId) {
|
||||
public Deployment createDeployment(UUID appId, UUID appVersionId, UUID environmentId, String createdBy) {
|
||||
App app = appService.getById(appId);
|
||||
Environment env = envService.getById(environmentId);
|
||||
String containerName = env.slug() + "-" + app.slug();
|
||||
|
||||
deployRepo.deleteFailedByAppAndEnvironment(appId, environmentId);
|
||||
UUID deploymentId = deployRepo.create(appId, appVersionId, environmentId, containerName);
|
||||
UUID deploymentId = deployRepo.create(appId, appVersionId, environmentId, containerName, createdBy);
|
||||
return deployRepo.findById(deploymentId).orElseThrow();
|
||||
}
|
||||
|
||||
/** Promote: deploy the same app version to a different environment. */
|
||||
public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId) {
|
||||
return createDeployment(appId, appVersionId, targetEnvironmentId);
|
||||
public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId, String createdBy) {
|
||||
return createDeployment(appId, appVersionId, targetEnvironmentId, createdBy);
|
||||
}
|
||||
|
||||
public void markRunning(UUID deploymentId, String containerId) {
|
||||
|
||||
Reference in New Issue
Block a user