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) {
|
@RequestBody DeployRequest request) {
|
||||||
try {
|
try {
|
||||||
App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
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);
|
deploymentExecutor.executeAsync(deployment);
|
||||||
return ResponseEntity.accepted().body(deployment);
|
return ResponseEntity.accepted().body(deployment);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -129,7 +129,7 @@ public class DeploymentController {
|
|||||||
Environment targetEnv = environmentService.getBySlug(request.targetEnvironment());
|
Environment targetEnv = environmentService.getBySlug(request.targetEnvironment());
|
||||||
// Target must also have the app with the same slug
|
// Target must also have the app with the same slug
|
||||||
App targetApp = appService.getByEnvironmentAndSlug(targetEnv.id(), appSlug);
|
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);
|
deploymentExecutor.executeAsync(promoted);
|
||||||
return ResponseEntity.accepted().body(promoted);
|
return ResponseEntity.accepted().body(promoted);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
|||||||
@@ -22,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_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 JdbcTemplate jdbc;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
@@ -81,10 +81,10 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
UUID id = UUID.randomUUID();
|
||||||
jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name) VALUES (?, ?, ?, ?, ?)",
|
jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name, created_by) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
id, appId, appVersionId, environmentId, containerName);
|
id, appId, appVersionId, environmentId, containerName, createdBy);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,8 @@ public class PostgresDeploymentRepository implements DeploymentRepository {
|
|||||||
deployedConfigSnapshot,
|
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(),
|
||||||
|
rs.getString("created_by")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class DeploymentStateEvaluatorTest {
|
|||||||
private Deployment deployment(DeploymentStatus status) {
|
private Deployment deployment(DeploymentStatus status) {
|
||||||
return new Deployment(DEP_ID, APP_ID, UUID.randomUUID(), ENV_ID, status,
|
return new Deployment(DEP_ID, APP_ID, UUID.randomUUID(), ENV_ID, status,
|
||||||
null, null, List.of(), null, null, "orders-0", null,
|
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
|
@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
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container");
|
UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container", null);
|
||||||
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
|
repository.saveDeployedConfigSnapshot(deploymentId, snapshot);
|
||||||
|
|
||||||
// when — load it back
|
// when — load it back
|
||||||
@@ -80,7 +80,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
|||||||
@Test
|
@Test
|
||||||
void deployedConfigSnapshot_nullByDefault() {
|
void deployedConfigSnapshot_nullByDefault() {
|
||||||
// deployments created without a snapshot must return null (not throw)
|
// 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();
|
Deployment loaded = repository.findById(deploymentId).orElseThrow();
|
||||||
|
|
||||||
@@ -90,13 +90,13 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
|||||||
@Test
|
@Test
|
||||||
void deleteFailedByAppAndEnvironment_keepsStoppedAndActive() {
|
void deleteFailedByAppAndEnvironment_keepsStoppedAndActive() {
|
||||||
// given: one STOPPED (checkpoint), one FAILED, one RUNNING
|
// 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);
|
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");
|
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);
|
repository.updateStatus(runningId, com.cameleer.server.core.runtime.DeploymentStatus.RUNNING, "c1", null);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -118,7 +118,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT {
|
|||||||
null
|
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, snapshot);
|
||||||
repository.saveDeployedConfigSnapshot(deploymentId, null);
|
repository.saveDeployedConfigSnapshot(deploymentId, null);
|
||||||
|
|
||||||
|
|||||||
@@ -22,19 +22,20 @@ public record Deployment(
|
|||||||
DeploymentConfigSnapshot deployedConfigSnapshot,
|
DeploymentConfigSnapshot deployedConfigSnapshot,
|
||||||
Instant deployedAt,
|
Instant deployedAt,
|
||||||
Instant stoppedAt,
|
Instant stoppedAt,
|
||||||
Instant createdAt
|
Instant createdAt,
|
||||||
|
String createdBy
|
||||||
) {
|
) {
|
||||||
public Deployment withStatus(DeploymentStatus newStatus) {
|
public Deployment withStatus(DeploymentStatus newStatus) {
|
||||||
return new Deployment(id, appId, appVersionId, environmentId, newStatus,
|
return new Deployment(id, appId, appVersionId, environmentId, newStatus,
|
||||||
targetState, deploymentStrategy, replicaStates, deployStage,
|
targetState, deploymentStrategy, replicaStates, deployStage,
|
||||||
containerId, containerName, errorMessage, resolvedConfig,
|
containerId, containerName, errorMessage, resolvedConfig,
|
||||||
deployedConfigSnapshot, deployedAt, stoppedAt, createdAt);
|
deployedConfigSnapshot, deployedAt, stoppedAt, createdAt, createdBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Deployment withDeployedConfigSnapshot(DeploymentConfigSnapshot snapshot) {
|
public Deployment withDeployedConfigSnapshot(DeploymentConfigSnapshot snapshot) {
|
||||||
return new Deployment(id, appId, appVersionId, environmentId, status,
|
return new Deployment(id, appId, appVersionId, environmentId, status,
|
||||||
targetState, deploymentStrategy, replicaStates, deployStage,
|
targetState, deploymentStrategy, replicaStates, deployStage,
|
||||||
containerId, containerName, errorMessage, resolvedConfig,
|
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> findById(UUID id);
|
||||||
Optional<Deployment> findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId);
|
Optional<Deployment> findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId);
|
||||||
Optional<Deployment> findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId);
|
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 updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage);
|
||||||
void markDeployed(UUID id);
|
void markDeployed(UUID id);
|
||||||
void markStopped(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)); }
|
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). */
|
/** 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);
|
App app = appService.getById(appId);
|
||||||
Environment env = envService.getById(environmentId);
|
Environment env = envService.getById(environmentId);
|
||||||
String containerName = env.slug() + "-" + app.slug();
|
String containerName = env.slug() + "-" + app.slug();
|
||||||
|
|
||||||
deployRepo.deleteFailedByAppAndEnvironment(appId, environmentId);
|
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();
|
return deployRepo.findById(deploymentId).orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Promote: deploy the same app version to a different environment. */
|
/** Promote: deploy the same app version to a different environment. */
|
||||||
public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId) {
|
public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId, String createdBy) {
|
||||||
return createDeployment(appId, appVersionId, targetEnvironmentId);
|
return createDeployment(appId, appVersionId, targetEnvironmentId, createdBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markRunning(UUID deploymentId, String containerId) {
|
public void markRunning(UUID deploymentId, String containerId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user