api(apps): GET /apps/{slug}/dirty-state returns desired-vs-deployed diff

Wires DirtyStateCalculator behind an HTTP endpoint on AppController.
Adds findLatestSuccessfulByAppAndEnv to PostgresDeploymentRepository,
registers DirtyStateCalculator as a Spring bean (with ObjectMapper for
JavaTimeModule support), and covers all three scenarios with IT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:35:35 +02:00
parent 24464c0772
commit 6591f2fde3
8 changed files with 353 additions and 16 deletions

View File

@@ -16,11 +16,17 @@ import java.util.UUID;
* Compares the app's current desired state (JAR + agent config + container config) to the
* config snapshot from the last successful deployment, producing a structured dirty result.
*
* <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.</p>
* <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.
* Caller must supply an {@link ObjectMapper} configured with {@code JavaTimeModule} so that
* {@code ApplicationConfig.updatedAt} (an {@link java.time.Instant}) serialises correctly.</p>
*/
public class DirtyStateCalculator {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final ObjectMapper mapper;
public DirtyStateCalculator(ObjectMapper mapper) {
this.mapper = mapper;
}
public DirtyStateResult compute(UUID desiredJarVersionId,
ApplicationConfig desiredAgentConfig,
@@ -38,10 +44,10 @@ public class DirtyStateCalculator {
String.valueOf(desiredJarVersionId), String.valueOf(snapshot.jarVersionId())));
}
compareJson("agentConfig", MAPPER.valueToTree(desiredAgentConfig),
MAPPER.valueToTree(snapshot.agentConfig()), diffs);
compareJson("containerConfig", MAPPER.valueToTree(desiredContainerConfig),
MAPPER.valueToTree(snapshot.containerConfig()), diffs);
compareJson("agentConfig", mapper.valueToTree(desiredAgentConfig),
mapper.valueToTree(snapshot.agentConfig()), diffs);
compareJson("containerConfig", mapper.valueToTree(desiredContainerConfig),
mapper.valueToTree(snapshot.containerConfig()), diffs);
return new DirtyStateResult(!diffs.isEmpty(), diffs);
}

View File

@@ -1,6 +1,7 @@
package com.cameleer.server.core.runtime;
import com.cameleer.common.model.ApplicationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Map;
@@ -10,9 +11,11 @@ import static org.assertj.core.api.Assertions.assertThat;
class DirtyStateCalculatorTest {
private static final DirtyStateCalculator CALC = new DirtyStateCalculator(new ObjectMapper());
@Test
void noSnapshot_meansEverythingDirty() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig desiredAgent = new ApplicationConfig();
desiredAgent.setSamplingRate(1.0);
@@ -27,7 +30,7 @@ class DirtyStateCalculatorTest {
@Test
void identicalSnapshot_isClean() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig();
cfg.setSamplingRate(0.5);
@@ -43,7 +46,7 @@ class DirtyStateCalculatorTest {
@Test
void differentJar_marksJarField() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig();
Map<String, Object> container = Map.of();
UUID v1 = UUID.randomUUID();
@@ -59,7 +62,7 @@ class DirtyStateCalculatorTest {
@Test
void differentSamplingRate_marksAgentField() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig deployedCfg = new ApplicationConfig();
deployedCfg.setSamplingRate(0.5);
@@ -77,7 +80,7 @@ class DirtyStateCalculatorTest {
@Test
void differentContainerMemory_marksContainerField() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig cfg = new ApplicationConfig();
UUID jarId = UUID.randomUUID();
DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, Map.of("memoryLimitMb", 512));
@@ -91,7 +94,7 @@ class DirtyStateCalculatorTest {
@Test
void nullAgentConfigInSnapshot_marksAgentConfigDiff() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig desired = new ApplicationConfig();
desired.setSamplingRate(1.0);
UUID jarId = UUID.randomUUID();
@@ -106,7 +109,7 @@ class DirtyStateCalculatorTest {
@Test
void nestedAgentField_reportsDeepPath() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig deployed = new ApplicationConfig();
deployed.setTracedProcessors(Map.of("proc-1", "DEBUG"));
@@ -124,7 +127,7 @@ class DirtyStateCalculatorTest {
@Test
void stringField_differenceValueIsUnquoted() {
DirtyStateCalculator calc = new DirtyStateCalculator();
DirtyStateCalculator calc = CALC;
ApplicationConfig deployed = new ApplicationConfig();
deployed.setApplicationLogLevel("INFO");