feat(audit): audit deploy/stop/promote with DEPLOYMENT category
Wires AuditService and AppVersionRepository into DeploymentController. Replaces null createdBy placeholder with currentUserId() on createDeployment/promote. Adds audit log entries (SUCCESS + FAILURE) for deploy_app, stop_deployment, and promote_deployment actions. Fixes FK violations in affected ITs by seeding the test-operator and alice users into the users table before deploy calls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,13 @@ package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer.server.app.runtime.DeploymentExecutor;
|
||||
import com.cameleer.server.app.web.EnvPath;
|
||||
import com.cameleer.server.core.admin.AuditCategory;
|
||||
import com.cameleer.server.core.admin.AuditResult;
|
||||
import com.cameleer.server.core.admin.AuditService;
|
||||
import com.cameleer.server.core.runtime.App;
|
||||
import com.cameleer.server.core.runtime.AppService;
|
||||
import com.cameleer.server.core.runtime.AppVersion;
|
||||
import com.cameleer.server.core.runtime.AppVersionRepository;
|
||||
import com.cameleer.server.core.runtime.Deployment;
|
||||
import com.cameleer.server.core.runtime.DeploymentService;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
@@ -12,14 +17,18 @@ import com.cameleer.server.core.runtime.RuntimeOrchestrator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -42,17 +51,23 @@ public class DeploymentController {
|
||||
private final RuntimeOrchestrator orchestrator;
|
||||
private final AppService appService;
|
||||
private final EnvironmentService environmentService;
|
||||
private final AuditService auditService;
|
||||
private final AppVersionRepository appVersionRepository;
|
||||
|
||||
public DeploymentController(DeploymentService deploymentService,
|
||||
DeploymentExecutor deploymentExecutor,
|
||||
RuntimeOrchestrator orchestrator,
|
||||
AppService appService,
|
||||
EnvironmentService environmentService) {
|
||||
EnvironmentService environmentService,
|
||||
AuditService auditService,
|
||||
AppVersionRepository appVersionRepository) {
|
||||
this.deploymentService = deploymentService;
|
||||
this.deploymentExecutor = deploymentExecutor;
|
||||
this.orchestrator = orchestrator;
|
||||
this.appService = appService;
|
||||
this.environmentService = environmentService;
|
||||
this.auditService = auditService;
|
||||
this.appVersionRepository = appVersionRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -86,13 +101,25 @@ public class DeploymentController {
|
||||
@ApiResponse(responseCode = "202", description = "Deployment accepted and starting")
|
||||
public ResponseEntity<Deployment> deploy(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@RequestBody DeployRequest request) {
|
||||
@RequestBody DeployRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), null);
|
||||
AppVersion appVersion = appVersionRepository.findById(request.appVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("AppVersion not found: " + request.appVersionId()));
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), currentUserId());
|
||||
deploymentExecutor.executeAsync(deployment);
|
||||
auditService.log("deploy_app", AuditCategory.DEPLOYMENT, deployment.id().toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(),
|
||||
"appVersionId", request.appVersionId().toString(),
|
||||
"jarFilename", appVersion.jarFilename() != null ? appVersion.jarFilename() : "",
|
||||
"version", appVersion.version()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.accepted().body(deployment);
|
||||
} catch (IllegalArgumentException e) {
|
||||
auditService.log("deploy_app", AuditCategory.DEPLOYMENT, null,
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(), "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -103,12 +130,19 @@ public class DeploymentController {
|
||||
@ApiResponse(responseCode = "404", description = "Deployment not found")
|
||||
public ResponseEntity<Deployment> stop(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@PathVariable UUID deploymentId) {
|
||||
@PathVariable UUID deploymentId,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
Deployment deployment = deploymentService.getById(deploymentId);
|
||||
deploymentExecutor.stopDeployment(deployment);
|
||||
auditService.log("stop_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(deploymentService.getById(deploymentId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
auditService.log("stop_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(), "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -122,18 +156,25 @@ public class DeploymentController {
|
||||
public ResponseEntity<?> promote(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@PathVariable UUID deploymentId,
|
||||
@RequestBody PromoteRequest request) {
|
||||
@RequestBody PromoteRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
App sourceApp = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
||||
Deployment source = deploymentService.getById(deploymentId);
|
||||
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(), null);
|
||||
Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id(), currentUserId());
|
||||
deploymentExecutor.executeAsync(promoted);
|
||||
auditService.log("promote_deployment", AuditCategory.DEPLOYMENT, promoted.id().toString(),
|
||||
Map.of("sourceEnv", env.slug(), "targetEnv", request.targetEnvironment(),
|
||||
"appSlug", appSlug, "appVersionId", source.appVersionId().toString()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.accepted().body(promoted);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.status(org.springframework.http.HttpStatus.NOT_FOUND)
|
||||
auditService.log("promote_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -157,6 +198,15 @@ public class DeploymentController {
|
||||
}
|
||||
}
|
||||
|
||||
private String currentUserId() {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getName() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No authentication");
|
||||
}
|
||||
String name = auth.getName();
|
||||
return name.startsWith("user:") ? name.substring(5) : name;
|
||||
}
|
||||
|
||||
public record DeployRequest(UUID appVersionId) {}
|
||||
public record PromoteRequest(String targetEnvironment) {}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ class AppDirtyStateIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM app_versions");
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");
|
||||
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import com.cameleer.server.app.TestSecurityHelper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DeploymentControllerAuditIT extends AbstractPostgresIT {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private TestSecurityHelper securityHelper;
|
||||
|
||||
private String aliceJwt;
|
||||
private String adminJwt;
|
||||
private String appSlug;
|
||||
private String versionId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Mint JWT for alice (OPERATOR) — subject must start with "user:" for JwtAuthenticationFilter
|
||||
aliceJwt = securityHelper.createToken("user:alice", "user", List.of("OPERATOR"));
|
||||
adminJwt = securityHelper.adminToken();
|
||||
|
||||
// Clean up deployment-related tables
|
||||
jdbcTemplate.update("DELETE FROM deployments");
|
||||
jdbcTemplate.update("DELETE FROM app_versions");
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM audit_log");
|
||||
|
||||
// Ensure alice exists in the users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('alice', 'local', 'Alice Test') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Create app in the seeded "default" environment
|
||||
appSlug = "audit-test-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
String appJson = String.format("""
|
||||
{"slug": "%s", "displayName": "Audit Test App"}
|
||||
""", appSlug);
|
||||
ResponseEntity<String> appResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps", HttpMethod.POST,
|
||||
new HttpEntity<>(appJson, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(appResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
// Upload a JAR version
|
||||
byte[] jarContent = "fake-jar-for-audit-test".getBytes();
|
||||
ByteArrayResource resource = new ByteArrayResource(jarContent) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "audit-test.jar";
|
||||
}
|
||||
};
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", resource);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + aliceJwt);
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
ResponseEntity<String> versionResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/versions", HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
String.class);
|
||||
assertThat(versionResponse.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
versionId = objectMapper.readTree(versionResponse.getBody()).path("id").asText();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploy_writes_audit_row_with_DEPLOYMENT_category_and_alice_actor() throws Exception {
|
||||
String json = String.format("""
|
||||
{"appVersionId": "%s"}
|
||||
""", versionId);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments", HttpMethod.POST,
|
||||
new HttpEntity<>(json, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||
|
||||
Map<String, Object> row = queryAuditRow("deploy_app");
|
||||
assertThat(row).isNotNull();
|
||||
assertThat(row.get("username")).isEqualTo("alice");
|
||||
assertThat(row.get("action")).isEqualTo("deploy_app");
|
||||
assertThat(row.get("category")).isEqualTo("DEPLOYMENT");
|
||||
assertThat(row.get("result")).isEqualTo("SUCCESS");
|
||||
assertThat(row.get("target")).isNotNull();
|
||||
assertThat(row.get("target").toString()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_writes_audit_row() throws Exception {
|
||||
// First deploy
|
||||
String deployJson = String.format("""
|
||||
{"appVersionId": "%s"}
|
||||
""", versionId);
|
||||
ResponseEntity<String> deployResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments", HttpMethod.POST,
|
||||
new HttpEntity<>(deployJson, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(deployResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||
String deploymentId = objectMapper.readTree(deployResponse.getBody()).path("id").asText();
|
||||
|
||||
// Clear audit log to isolate stop audit row
|
||||
jdbcTemplate.update("DELETE FROM audit_log");
|
||||
|
||||
// Stop the deployment
|
||||
ResponseEntity<String> stopResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments/" + deploymentId + "/stop",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(authHeadersNoBody(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(stopResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
Map<String, Object> row = queryAuditRow("stop_deployment");
|
||||
assertThat(row).isNotNull();
|
||||
assertThat(row.get("username")).isEqualTo("alice");
|
||||
assertThat(row.get("action")).isEqualTo("stop_deployment");
|
||||
assertThat(row.get("category")).isEqualTo("DEPLOYMENT");
|
||||
assertThat(row.get("result")).isEqualTo("SUCCESS");
|
||||
assertThat(row.get("target").toString()).isEqualTo(deploymentId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void promote_writes_audit_row() throws Exception {
|
||||
// Create a second environment for promotion target
|
||||
String targetEnvSlug = "promote-target-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
String envJson = String.format("""
|
||||
{"slug": "%s", "displayName": "Promote Target Env"}
|
||||
""", targetEnvSlug);
|
||||
ResponseEntity<String> envResponse = restTemplate.exchange(
|
||||
"/api/v1/admin/environments", HttpMethod.POST,
|
||||
new HttpEntity<>(envJson, authHeaders(adminJwt)),
|
||||
String.class);
|
||||
assertThat(envResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
// Create the same app slug in the target environment
|
||||
String appJson = String.format("""
|
||||
{"slug": "%s", "displayName": "Audit Test App (target)"}
|
||||
""", appSlug);
|
||||
ResponseEntity<String> targetAppResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/" + targetEnvSlug + "/apps", HttpMethod.POST,
|
||||
new HttpEntity<>(appJson, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(targetAppResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
// Deploy in source (default) env
|
||||
String deployJson = String.format("""
|
||||
{"appVersionId": "%s"}
|
||||
""", versionId);
|
||||
ResponseEntity<String> deployResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments", HttpMethod.POST,
|
||||
new HttpEntity<>(deployJson, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(deployResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||
String deploymentId = objectMapper.readTree(deployResponse.getBody()).path("id").asText();
|
||||
|
||||
// Clear audit log to isolate promote audit row
|
||||
jdbcTemplate.update("DELETE FROM audit_log");
|
||||
|
||||
// Promote to target env
|
||||
String promoteJson = String.format("""
|
||||
{"targetEnvironment": "%s"}
|
||||
""", targetEnvSlug);
|
||||
ResponseEntity<String> promoteResponse = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments/" + deploymentId + "/promote",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(promoteJson, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
assertThat(promoteResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||
|
||||
Map<String, Object> row = queryAuditRow("promote_deployment");
|
||||
assertThat(row).isNotNull();
|
||||
assertThat(row.get("username")).isEqualTo("alice");
|
||||
assertThat(row.get("action")).isEqualTo("promote_deployment");
|
||||
assertThat(row.get("category")).isEqualTo("DEPLOYMENT");
|
||||
assertThat(row.get("result")).isEqualTo("SUCCESS");
|
||||
assertThat(row.get("target")).isNotNull();
|
||||
assertThat(row.get("target").toString()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploy_with_unknown_appVersion_writes_FAILURE_audit_row() throws Exception {
|
||||
String unknownVersionId = UUID.randomUUID().toString();
|
||||
String json = String.format("""
|
||||
{"appVersionId": "%s"}
|
||||
""", unknownVersionId);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/environments/default/apps/" + appSlug + "/deployments", HttpMethod.POST,
|
||||
new HttpEntity<>(json, authHeaders(aliceJwt)),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
Map<String, Object> row = queryAuditRow("deploy_app");
|
||||
assertThat(row).isNotNull();
|
||||
assertThat(row.get("username")).isEqualTo("alice");
|
||||
assertThat(row.get("action")).isEqualTo("deploy_app");
|
||||
assertThat(row.get("category")).isEqualTo("DEPLOYMENT");
|
||||
assertThat(row.get("result")).isEqualTo("FAILURE");
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private HttpHeaders authHeaders(String jwt) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + jwt);
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
return headers;
|
||||
}
|
||||
|
||||
private HttpHeaders authHeadersNoBody(String jwt) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + jwt);
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Query the most recent audit_log row for the given action. Returns null if not found. */
|
||||
private Map<String, Object> queryAuditRow(String action) {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT username, action, category, target, result FROM audit_log WHERE action = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
action);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,10 @@ class DeploymentControllerIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM app_versions");
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
// Get default environment ID
|
||||
ResponseEntity<String> envResponse = restTemplate.exchange(
|
||||
"/api/v1/admin/environments", HttpMethod.GET,
|
||||
|
||||
@@ -62,6 +62,10 @@ class BlueGreenStrategyIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");
|
||||
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||
|
||||
appSlug = "bg-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
@@ -69,6 +69,10 @@ class DeploymentSnapshotIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM app_versions");
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");
|
||||
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -65,6 +65,10 @@ class RollingStrategyIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM apps");
|
||||
jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");
|
||||
|
||||
// Ensure test-operator exists in users table (required for deployments.created_by FK)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
|
||||
|
||||
when(runtimeOrchestrator.isEnabled()).thenReturn(true);
|
||||
|
||||
appSlug = "roll-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
Reference in New Issue
Block a user