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)
|
||||
- `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)`.
|
||||
- `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.
|
||||
|
||||
@@ -26,7 +26,8 @@ public class PostgresEnvironmentRepository implements EnvironmentRepository {
|
||||
}
|
||||
|
||||
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
|
||||
public List<Environment> findAll() {
|
||||
@@ -113,7 +114,10 @@ public class PostgresEnvironmentRepository implements EnvironmentRepository {
|
||||
config,
|
||||
jarRetentionCount,
|
||||
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);
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class ExchangeMatchEvaluatorTest {
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class LogPatternEvaluatorTest {
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class RouteMetricEvaluatorTest {
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class NotificationContextBuilderTest {
|
||||
// ---- helpers ----
|
||||
|
||||
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) {
|
||||
|
||||
@@ -105,6 +105,25 @@ class EnvironmentAdminControllerIT extends AbstractPostgresIT {
|
||||
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
|
||||
void updateEnvironment_withValidColor_persists() throws Exception {
|
||||
restTemplate.exchange(
|
||||
|
||||
@@ -13,5 +13,23 @@ public record Environment(
|
||||
Map<String, Object> defaultContainerConfig,
|
||||
Integer jarRetentionCount,
|
||||
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