feat(license): surface execution/log/metric retention days on Environment
Adds three int fields to the Environment record + repository row mapper, matching the columns added in V5. Default value is 1 per the V5 NOT NULL DEFAULT 1. Read DTO surfaces the fields via Jackson record serialization; setter endpoint deferred to a follow-up that wires the corresponding license cap checks. The canonical constructor enforces >= 1 for each retention field — V5 guarantees this at the DB level, but the runtime guard catches in-memory construction errors (e.g., test sites that pass 0). Test sites updated to the 12-arg signature with retention defaults of 1. EnvironmentAdminControllerIT gains a regression test asserting the wire shape exposes all three fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ paths:
|
|||||||
|
|
||||||
- `App` — record: id, environmentId, slug, displayName, containerConfig (JSONB)
|
- `App` — record: id, environmentId, slug, displayName, containerConfig (JSONB)
|
||||||
- `AppVersion` — record: id, appId, version, jarPath, detectedRuntimeType, detectedMainClass
|
- `AppVersion` — record: id, appId, version, jarPath, detectedRuntimeType, detectedMainClass
|
||||||
- `Environment` — record: id, slug, displayName, production, enabled, defaultContainerConfig, jarRetentionCount, color, createdAt. `color` is one of the 8 preset palette values validated by `EnvironmentColor.VALUES` and CHECK-constrained in PostgreSQL (V2 migration).
|
- `Environment` — record: id, slug, displayName, production, enabled, defaultContainerConfig, jarRetentionCount, color, createdAt, executionRetentionDays, logRetentionDays, metricRetentionDays. `color` is one of the 8 preset palette values validated by `EnvironmentColor.VALUES` and CHECK-constrained in PostgreSQL (V2 migration). The 3 retention day fields (V5) are `int`-typed (not nullable, since unlimited has no use-case), default to 1 day per the V5 `NOT NULL DEFAULT 1`, validated >= 1 in the canonical constructor.
|
||||||
- `EnvironmentColor` — constants: `DEFAULT = "slate"`, `VALUES = {slate,red,amber,green,teal,blue,purple,pink}`, `isValid(String)`.
|
- `EnvironmentColor` — constants: `DEFAULT = "slate"`, `VALUES = {slate,red,amber,green,teal,blue,purple,pink}`, `isValid(String)`.
|
||||||
- `Deployment` — record: id, appId, appVersionId, environmentId, status, targetState, deploymentStrategy, replicaStates (JSONB), deployStage, containerId, containerName, createdBy (String, user_id reference; nullable for pre-V4 historical rows)
|
- `Deployment` — record: id, appId, appVersionId, environmentId, status, targetState, deploymentStrategy, replicaStates (JSONB), deployStage, containerId, containerName, createdBy (String, user_id reference; nullable for pre-V4 historical rows)
|
||||||
- `DeploymentStatus` — enum: STOPPED, STARTING, RUNNING, DEGRADED, STOPPING, FAILED. `DEGRADED` is reserved for post-deploy drift (a replica died after RUNNING); `DeploymentExecutor` now marks partial-healthy deploys FAILED, not DEGRADED.
|
- `DeploymentStatus` — enum: STOPPED, STARTING, RUNNING, DEGRADED, STOPPING, FAILED. `DEGRADED` is reserved for post-deploy drift (a replica died after RUNNING); `DeploymentExecutor` now marks partial-healthy deploys FAILED, not DEGRADED.
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public class PostgresEnvironmentRepository implements EnvironmentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final String SELECT_COLS =
|
private static final String SELECT_COLS =
|
||||||
"id, slug, display_name, production, enabled, default_container_config, jar_retention_count, color, created_at";
|
"id, slug, display_name, production, enabled, default_container_config, jar_retention_count, color, created_at, "
|
||||||
|
+ "execution_retention_days, log_retention_days, metric_retention_days";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Environment> findAll() {
|
public List<Environment> findAll() {
|
||||||
@@ -113,7 +114,10 @@ public class PostgresEnvironmentRepository implements EnvironmentRepository {
|
|||||||
config,
|
config,
|
||||||
jarRetentionCount,
|
jarRetentionCount,
|
||||||
color,
|
color,
|
||||||
rs.getTimestamp("created_at").toInstant()
|
rs.getTimestamp("created_at").toInstant(),
|
||||||
|
rs.getInt("execution_retention_days"),
|
||||||
|
rs.getInt("log_retention_days"),
|
||||||
|
rs.getInt("metric_retention_days")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AgentLifecycleEvaluatorTest {
|
|||||||
events = mock(AgentEventRepository.class);
|
events = mock(AgentEventRepository.class);
|
||||||
envRepo = mock(EnvironmentRepository.class);
|
envRepo = mock(EnvironmentRepository.class);
|
||||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(
|
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(
|
||||||
new Environment(ENV_ID, ENV_SLUG, "Prod", true, true, Map.of(), 5, "slate", Instant.EPOCH)));
|
new Environment(ENV_ID, ENV_SLUG, "Prod", true, true, Map.of(), 5, "slate", Instant.EPOCH, 1, 1, 1)));
|
||||||
eval = new AgentLifecycleEvaluator(events, envRepo);
|
eval = new AgentLifecycleEvaluator(events, envRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ExchangeMatchEvaluatorTest {
|
|||||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||||
eval = new ExchangeMatchEvaluator(searchIndex, envRepo, props);
|
eval = new ExchangeMatchEvaluator(searchIndex, envRepo, props);
|
||||||
|
|
||||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null, 1, 1, 1);
|
||||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class LogPatternEvaluatorTest {
|
|||||||
envRepo = mock(EnvironmentRepository.class);
|
envRepo = mock(EnvironmentRepository.class);
|
||||||
eval = new LogPatternEvaluator(logStore, envRepo);
|
eval = new LogPatternEvaluator(logStore, envRepo);
|
||||||
|
|
||||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null, 1, 1, 1);
|
||||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class RouteMetricEvaluatorTest {
|
|||||||
envRepo = mock(EnvironmentRepository.class);
|
envRepo = mock(EnvironmentRepository.class);
|
||||||
eval = new RouteMetricEvaluator(statsStore, envRepo);
|
eval = new RouteMetricEvaluator(statsStore, envRepo);
|
||||||
|
|
||||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null, 1, 1, 1);
|
||||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class NotificationContextBuilderTest {
|
|||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
private Environment env() {
|
private Environment env() {
|
||||||
return new Environment(ENV_ID, "prod", "Production", true, true, Map.of(), 5, "slate", Instant.EPOCH);
|
return new Environment(ENV_ID, "prod", "Production", true, true, Map.of(), 5, "slate", Instant.EPOCH, 1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AlertRule rule(ConditionKind kind) {
|
private AlertRule rule(ConditionKind kind) {
|
||||||
|
|||||||
@@ -105,6 +105,25 @@ class EnvironmentAdminControllerIT extends AbstractPostgresIT {
|
|||||||
assertThat(body.has("id")).isTrue();
|
assertThat(body.has("id")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createEnvironment_surfacesRetentionDefaults() throws Exception {
|
||||||
|
// V5 columns default to 1 (matching the default-tier license cap). T26 surfaces
|
||||||
|
// them as int fields on the Environment record; the read DTO must expose them.
|
||||||
|
String json = """
|
||||||
|
{"slug": "retention-defaults", "displayName": "Retention", "production": false}
|
||||||
|
""";
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
|
"/api/v1/admin/environments", HttpMethod.POST,
|
||||||
|
new HttpEntity<>(json, securityHelper.authHeaders(adminJwt)),
|
||||||
|
String.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||||
|
JsonNode body = objectMapper.readTree(response.getBody());
|
||||||
|
assertThat(body.path("executionRetentionDays").asInt()).isEqualTo(1);
|
||||||
|
assertThat(body.path("logRetentionDays").asInt()).isEqualTo(1);
|
||||||
|
assertThat(body.path("metricRetentionDays").asInt()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void updateEnvironment_withValidColor_persists() throws Exception {
|
void updateEnvironment_withValidColor_persists() throws Exception {
|
||||||
restTemplate.exchange(
|
restTemplate.exchange(
|
||||||
|
|||||||
@@ -13,5 +13,23 @@ public record Environment(
|
|||||||
Map<String, Object> defaultContainerConfig,
|
Map<String, Object> defaultContainerConfig,
|
||||||
Integer jarRetentionCount,
|
Integer jarRetentionCount,
|
||||||
String color,
|
String color,
|
||||||
Instant createdAt
|
Instant createdAt,
|
||||||
) {}
|
int executionRetentionDays,
|
||||||
|
int logRetentionDays,
|
||||||
|
int metricRetentionDays
|
||||||
|
) {
|
||||||
|
public Environment {
|
||||||
|
if (executionRetentionDays < 1) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"executionRetentionDays must be >= 1 (got " + executionRetentionDays + ")");
|
||||||
|
}
|
||||||
|
if (logRetentionDays < 1) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"logRetentionDays must be >= 1 (got " + logRetentionDays + ")");
|
||||||
|
}
|
||||||
|
if (metricRetentionDays < 1) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"metricRetentionDays must be >= 1 (got " + metricRetentionDays + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user